blob: 4d80419d233c649a0aea4f1f23f45746847a31a9 [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
Nils Diewald86dad5b2015-01-28 15:09:07 +000031 /**
32 * List of items for drop down menu (complete).
33 * Only a sublist of the menu is filtered (live).
34 * Only a sublist of the filtered menu is visible (shown).
35 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000036 return {
Nils Diewald86dad5b2015-01-28 15:09:07 +000037 /**
38 * Create new Menu based on the action prefix
39 * and a list of menu items.
40 *
Akron7524be12016-06-01 17:31:33 +020041 *
42 * Accepts an associative array containg the elements
43 * itemClass, prefixClass, lengthFieldClass
44 *
Nils Diewald86dad5b2015-01-28 15:09:07 +000045 * @this {Menu}
46 * @constructor
47 * @param {string} Context prefix
48 * @param {Array.<Array.<string>>} List of menu items
49 */
Akron7524be12016-06-01 17:31:33 +020050 create : function (list, params) {
51 return Object.create(this)._init(list, params);
Nils Diewald86dad5b2015-01-28 15:09:07 +000052 },
53
Akron5240b8c2016-05-20 09:17:41 +020054 // Initialize list
Akron7524be12016-06-01 17:31:33 +020055 _init : function (list, params) {
Akrona92fd8d2016-05-24 21:13:41 +020056
Akron7524be12016-06-01 17:31:33 +020057 if (params === undefined)
Akrone4961b12017-05-10 21:04:46 +020058 params = {};
Akron7524be12016-06-01 17:31:33 +020059
60 this._itemClass = params["itemClass"] || defaultItemClass;
Akron5240b8c2016-05-20 09:17:41 +020061
62 // Add prefix object
Akron7524be12016-06-01 17:31:33 +020063 if (params["prefixClass"] !== undefined) {
Akrone4961b12017-05-10 21:04:46 +020064 this._prefix = params["prefixClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020065 }
66 else {
Akrone4961b12017-05-10 21:04:46 +020067 this._prefix = defaultPrefixClass.create();
Akron5240b8c2016-05-20 09:17:41 +020068 };
69 this._prefix._menu = this;
70
71 // Add lengthField object
Akron7524be12016-06-01 17:31:33 +020072 if (params["lengthFieldClass"] !== undefined) {
Akrone4961b12017-05-10 21:04:46 +020073 this._lengthField = params["lengthFieldClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020074 }
75 else {
Akrone4961b12017-05-10 21:04:46 +020076 this._lengthField = defaultLengthFieldClass.create();
Akron5240b8c2016-05-20 09:17:41 +020077 };
78 this._lengthField._menu = this;
79
80 // Initialize slider
81 this._slider = sliderClass.create(this);
82
83 // Create the element
Akron9c4d1ae2016-05-25 21:43:22 +020084 var el = document.createElement("ul");
85 with (el) {
Akrone4961b12017-05-10 21:04:46 +020086 style.outline = 0;
87 setAttribute('tabindex', 0);
88 classList.add('menu', 'roll');
89 appendChild(this._prefix.element());
90 appendChild(this._lengthField.element());
91 appendChild(this._slider.element());
Akron9c4d1ae2016-05-25 21:43:22 +020092 };
Akron5240b8c2016-05-20 09:17:41 +020093
94 // This has to be cleaned up later on
Akron9c4d1ae2016-05-25 21:43:22 +020095 el["menu"] = this;
Akron5240b8c2016-05-20 09:17:41 +020096
97 // Arrow keys
Akron9c4d1ae2016-05-25 21:43:22 +020098 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +020099 'keydown',
100 this._keydown.bind(this),
101 false
Akron5240b8c2016-05-20 09:17:41 +0200102 );
103
104 // Strings
Akron9c4d1ae2016-05-25 21:43:22 +0200105 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200106 'keypress',
107 this._keypress.bind(this),
108 false
Akron5240b8c2016-05-20 09:17:41 +0200109 );
110
111 // Mousewheel
Akron9c4d1ae2016-05-25 21:43:22 +0200112 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200113 'wheel',
114 this._mousewheel.bind(this),
115 false
Akron5240b8c2016-05-20 09:17:41 +0200116 );
Akrona1159ff2018-07-22 13:28:31 +0200117
118 // Touch
119 el.addEventListener(
120 'touchstart',
121 this._touch.bind(this),
122 false
123 );
124 el.addEventListener(
125 'touchend',
126 this._touch.bind(this),
127 false
128 );
129 el.addEventListener(
130 'touchmove',
131 this._touch.bind(this),
132 false
133 );
134
135
Akron9c4d1ae2016-05-25 21:43:22 +0200136 this._element = el;
Akroneaba63e2018-01-26 19:49:30 +0100137
138 this._limit = menuLimit;
Akrone4961b12017-05-10 21:04:46 +0200139
Akron5240b8c2016-05-20 09:17:41 +0200140 this._items = new Array();
Akron5240b8c2016-05-20 09:17:41 +0200141
Akroneaba63e2018-01-26 19:49:30 +0100142 // TODO:
143 // Make this separate from _init
144 this.readItems(list);
145
hebastaf95226b2019-09-19 11:37:00 +0200146 this.dontHide = false;
147
Akroneaba63e2018-01-26 19:49:30 +0100148 return this;
149 },
150
151 // Read items to add to list
152 readItems : function (list) {
153
154 this._list = undefined;
155
156 // Remove circular reference to "this" in items
157 for (var i = 0; i < this._items.length; i++) {
158 delete this._items[i]["_menu"];
159 delete this._items[i];
160 };
161
162 this._items = new Array();
163 this.removeItems();
164
165
166 // Initialize items
167 this._lengthField.reset();
168
Akron9c4d1ae2016-05-25 21:43:22 +0200169 var i = 0;
Akron5240b8c2016-05-20 09:17:41 +0200170 // Initialize item list based on parameters
Akron678c26f2020-10-09 08:52:50 +0200171 list.forEach(function(i){
172 var obj = this._itemClass.create(i);
Akron5240b8c2016-05-20 09:17:41 +0200173
Akrone4961b12017-05-10 21:04:46 +0200174 // This may become circular
175 obj["_menu"] = this;
Akron678c26f2020-10-09 08:52:50 +0200176 this._lengthField.add(i);
Akrone4961b12017-05-10 21:04:46 +0200177 this._items.push(obj);
Akron678c26f2020-10-09 08:52:50 +0200178 }, this);
Akron5240b8c2016-05-20 09:17:41 +0200179
Akron9c4d1ae2016-05-25 21:43:22 +0200180 this._slider.length(this.liveLength())
Akrone4961b12017-05-10 21:04:46 +0200181 .limit(this._limit)
182 .reInit();
183
Akroneaba63e2018-01-26 19:49:30 +0100184 this._firstActive = false;
185 // Show the first item active always?
Akron9c4d1ae2016-05-25 21:43:22 +0200186 this.offset = 0;
187 this.position = 0;
Akron5240b8c2016-05-20 09:17:41 +0200188 },
Akroneaba63e2018-01-26 19:49:30 +0100189
Akron5240b8c2016-05-20 09:17:41 +0200190 // Initialize the item list
191 _initList : function () {
192
193 // Create a new list
194 if (this._list === undefined) {
Akrone4961b12017-05-10 21:04:46 +0200195 this._list = [];
Akron5240b8c2016-05-20 09:17:41 +0200196 }
197 else if (this._list.length !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200198 this._boundary(false);
199 this._list.length = 0;
Akron5240b8c2016-05-20 09:17:41 +0200200 };
201
202 // Offset is initially zero
Akron9c4d1ae2016-05-25 21:43:22 +0200203 this.offset = 0;
Akron5240b8c2016-05-20 09:17:41 +0200204
205 // There is no prefix set
206 if (this.prefix().length <= 0) {
207
Akrone4961b12017-05-10 21:04:46 +0200208 // add all items to the list and lowlight
Akronb50964a2020-10-12 11:44:37 +0200209 let i = 0;
Akrone4961b12017-05-10 21:04:46 +0200210 for (; i < this._items.length; i++) {
211 this._list.push(i);
212 this._items[i].lowlight();
213 };
Akron5240b8c2016-05-20 09:17:41 +0200214
Akroneaba63e2018-01-26 19:49:30 +0100215 this._slider.length(i).reInit();
Akron97752a72016-05-25 14:43:07 +0200216
Akrone4961b12017-05-10 21:04:46 +0200217 return true;
Akron5240b8c2016-05-20 09:17:41 +0200218 };
219
220 /*
221 * There is a prefix set, so filter the list!
222 */
223 var pos;
Akronacffc652017-12-18 21:21:25 +0100224 var prefixList = this.prefix().toLowerCase().split(" ");
225
226 var items = [];
227 var maxPoints = 1; // minimum 1
Akron5240b8c2016-05-20 09:17:41 +0200228
229 // Iterate over all items and choose preferred matching items
230 // i.e. the matching happens at the word start
Akronb50964a2020-10-12 11:44:37 +0200231 this._items.forEach(function(it, pos){
Akronacffc652017-12-18 21:21:25 +0100232
Akronb50964a2020-10-12 11:44:37 +0200233 let points = 0;
Akronacffc652017-12-18 21:21:25 +0100234
Akronb50964a2020-10-12 11:44:37 +0200235 prefixList.forEach(function(p) {
Akronacffc652017-12-18 21:21:25 +0100236
237 // Check if it matches at the beginning
Akronb50964a2020-10-12 11:44:37 +0200238 if ((it.lcField().includes(" " + p))) {
Akronacffc652017-12-18 21:21:25 +0100239 points += 5;
240 }
241
242 // Check if it matches anywhere
Akronb50964a2020-10-12 11:44:37 +0200243 else if (it.lcField().includes(p)) {
Akronacffc652017-12-18 21:21:25 +0100244 points += 1;
245 };
Akronb50964a2020-10-12 11:44:37 +0200246 });
Akronacffc652017-12-18 21:21:25 +0100247
248 if (points > maxPoints) {
249 this._list = [pos];
250 maxPoints = points;
251 }
252 else if (points == maxPoints) {
Akrone4961b12017-05-10 21:04:46 +0200253 this._list.push(pos);
Akronacffc652017-12-18 21:21:25 +0100254 }
Akronb50964a2020-10-12 11:44:37 +0200255 },this);
Akron5240b8c2016-05-20 09:17:41 +0200256
257 // The list is empty - so lower your expectations
258 // Iterate over all items and choose matching items
259 // i.e. the matching happens anywhere in the word
Akronacffc652017-12-18 21:21:25 +0100260 /*
Akron6ffad5d2016-05-24 17:16:58 +0200261 prefix = prefix.substring(1);
Akron5240b8c2016-05-20 09:17:41 +0200262 if (this._list.length == 0) {
Akrone4961b12017-05-10 21:04:46 +0200263 for (pos = 0; pos < this._items.length; pos++) {
264 if ((this.item(pos).lcField().indexOf(prefix)) >= 0)
265 this._list.push(pos);
266 };
Akron5240b8c2016-05-20 09:17:41 +0200267 };
Akronacffc652017-12-18 21:21:25 +0100268 */
Akron5240b8c2016-05-20 09:17:41 +0200269
Akron9c4d1ae2016-05-25 21:43:22 +0200270 this._slider.length(this._list.length).reInit();
Akron6ed13992016-05-23 18:06:05 +0200271
Akron5240b8c2016-05-20 09:17:41 +0200272 // Filter was successful - yeah!
273 return this._list.length > 0 ? true : false;
274 },
275
276
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000277 /**
278 * Destroy this menu
279 * (in case you don't trust the
280 * mark and sweep GC)!
281 */
282 destroy : function () {
Akron47c086c2016-05-18 21:22:06 +0200283
Akron5240b8c2016-05-20 09:17:41 +0200284 // Remove circular reference to "this" in menu
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000285 if (this._element != undefined)
Akrone4961b12017-05-10 21:04:46 +0200286 delete this._element["menu"];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000287
Akron5240b8c2016-05-20 09:17:41 +0200288 // Remove circular reference to "this" in items
Akron678c26f2020-10-09 08:52:50 +0200289 this._items.forEach(function(i) {
290 delete i["_menu"];
291 });
Akron5240b8c2016-05-20 09:17:41 +0200292
293 // Remove circular reference to "this" in prefix
Nils Diewald5c5a7472015-04-02 22:13:38 +0000294 delete this._prefix['_menu'];
Akron5240b8c2016-05-20 09:17:41 +0200295 delete this._lengthField['_menu'];
296 delete this._slider['_menu'];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000297 },
298
Nils Diewald7148c6f2015-05-04 15:07:53 +0000299
300 /**
301 * Focus on this menu.
302 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000303 focus : function () {
304 this._element.focus();
305 },
306
Nils Diewald7148c6f2015-05-04 15:07:53 +0000307
Nils Diewald59c02fc2015-03-07 01:29:09 +0000308 // mouse wheel treatment
309 _mousewheel : function (e) {
310 var delta = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000311 delta = e.deltaY / 120;
312 if (delta > 0)
Akrone4961b12017-05-10 21:04:46 +0200313 this.next();
Nils Diewald5975d702015-03-09 17:45:42 +0000314 else if (delta < 0)
Akrone4961b12017-05-10 21:04:46 +0200315 this.prev();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000316 e.halt();
317 },
318
Akrona1159ff2018-07-22 13:28:31 +0200319 // touchmove treatment
320 _touch : function (e) {
321 var s = this.slider();
322 if (e.type === "touchstart") {
323 // s.active(true);
324 var t = e.touches[0];
325 this._lastTouch = t.clientY;
326 }
327 else if (e.type === "touchend") {
328 // s.active(false);
329 this._lastTouch = undefined;
Akrona1159ff2018-07-22 13:28:31 +0200330 }
331 else if (e.type === "touchmove") {
332 var t = e.touches[0];
Akrone817b882018-08-31 14:09:17 +0200333
334 // TODO:
335 // Instead of using 26px, choose the item height
336 // or use the menu height // shownItems
337
Akrona1159ff2018-07-22 13:28:31 +0200338 // s.movetoRel(t.clientY - this._initTouch);
339 if ((this._lastTouch + 26) < t.clientY) {
Akrone817b882018-08-31 14:09:17 +0200340 this.viewDown();
Akrona1159ff2018-07-22 13:28:31 +0200341 this._lastTouch = t.clientY;
342 }
343 else if ((this._lastTouch - 26) > t.clientY) {
Akrone817b882018-08-31 14:09:17 +0200344 this.viewUp();
Akrona1159ff2018-07-22 13:28:31 +0200345 this._lastTouch = t.clientY;
346 }
347 e.halt();
348 };
349 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000350
Nils Diewald59c02fc2015-03-07 01:29:09 +0000351 // Arrow key and prefix treatment
Nils Diewald47f366b2015-04-15 20:06:35 +0000352 _keydown : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000353 var code = _codeFromEvent(e);
354
Nils Diewald59c02fc2015-03-07 01:29:09 +0000355 switch (code) {
356 case 27: // 'Esc'
Akrone4961b12017-05-10 21:04:46 +0200357 e.halt();
358 this.hide();
359 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000360
Nils Diewald59c02fc2015-03-07 01:29:09 +0000361 case 38: // 'Up'
Akrone4961b12017-05-10 21:04:46 +0200362 e.halt();
363 this.prev();
364 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000365 case 33: // 'Page up'
Akrone4961b12017-05-10 21:04:46 +0200366 e.halt();
367 this.pageUp();
368 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000369 case 40: // 'Down'
Akrone4961b12017-05-10 21:04:46 +0200370 e.halt();
371 this.next();
372 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000373 case 34: // 'Page down'
Akrone4961b12017-05-10 21:04:46 +0200374 e.halt();
375 this.pageDown();
376 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000377 case 39: // 'Right'
Akrone4961b12017-05-10 21:04:46 +0200378 if (this._prefix.active())
379 break;
Nils Diewalde8518f82015-03-18 22:41:49 +0000380
Akrone4961b12017-05-10 21:04:46 +0200381 var item = this.liveItem(this.position);
382
383 if (item["further"] !== undefined) {
384 item["further"].bind(item).apply();
385 };
386
387 e.halt();
388 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000389 case 13: // 'Enter'
390
Akrone4961b12017-05-10 21:04:46 +0200391 // Click on prefix
392 if (this._prefix.active())
393 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000394
Akrone4961b12017-05-10 21:04:46 +0200395 // Click on item
396 else
397 this.liveItem(this.position).onclick(e);
398 e.halt();
399 break;
Nils Diewald59c02fc2015-03-07 01:29:09 +0000400 case 8: // 'Backspace'
Akrone4961b12017-05-10 21:04:46 +0200401 this._prefix.chop();
402 this.show();
403 e.halt();
404 break;
Nils Diewald47f366b2015-04-15 20:06:35 +0000405 };
406 },
Nils Diewald59c02fc2015-03-07 01:29:09 +0000407
Nils Diewald47f366b2015-04-15 20:06:35 +0000408 // Add characters to prefix
409 _keypress : function (e) {
Akron9c2f9382016-05-25 16:36:04 +0200410 if (e.charCode !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200411 e.halt();
412 var c = String.fromCharCode(_codeFromEvent(e));
413
414 // Add prefix
415 this._prefix.add(c);
416 this.show();
Akron9c2f9382016-05-25 16:36:04 +0200417 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000418 },
419
Akron47c086c2016-05-18 21:22:06 +0200420 /**
Akron5240b8c2016-05-20 09:17:41 +0200421 * Show a screen with a given offset
422 * in the viewport.
Akron47c086c2016-05-18 21:22:06 +0200423 */
424 screen : function (nr) {
Akrone817b882018-08-31 14:09:17 +0200425
426 // Normalize negative values
Akron3c2730f2016-05-24 15:08:29 +0200427 if (nr < 0) {
Akrone4961b12017-05-10 21:04:46 +0200428 nr = 0
Akron3c2730f2016-05-24 15:08:29 +0200429 }
Akrone817b882018-08-31 14:09:17 +0200430
431 // The shown list already shows everything
432 else if (this.liveLength() < this.limit()) {
433 return false;
434 }
435
436 // Move relatively to the next screen
Akron6ac58442016-05-24 16:52:29 +0200437 else if (nr > (this.liveLength() - this.limit())) {
Akrone4961b12017-05-10 21:04:46 +0200438 nr = (this.liveLength() - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200439 };
440
Akrone817b882018-08-31 14:09:17 +0200441 // no change
Akron9c4d1ae2016-05-25 21:43:22 +0200442 if (this.offset === nr)
Akrone817b882018-08-31 14:09:17 +0200443 return false;
Akron5a1f5bb2016-05-23 22:00:39 +0200444
Akron47c086c2016-05-18 21:22:06 +0200445 this._showItems(nr);
Akrone817b882018-08-31 14:09:17 +0200446
447 return true;
Akron47c086c2016-05-18 21:22:06 +0200448 },
449
Akrone817b882018-08-31 14:09:17 +0200450
Nils Diewald2fe12e12015-03-06 16:47:06 +0000451 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000452 * Get the associated dom element.
Nils Diewald2fe12e12015-03-06 16:47:06 +0000453 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000454 element : function () {
455 return this._element;
456 },
457
Nils Diewald2fe12e12015-03-06 16:47:06 +0000458 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000459 * Get the creator class for items
Nils Diewald2fe12e12015-03-06 16:47:06 +0000460 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000461 itemClass : function () {
462 return this._itemClass;
463 },
464
465 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000466 * Get and set the numerical value
467 * for the maximum number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000468 */
469 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000470 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200471 if (this._limit !== limit) {
472 this._limit = limit;
473 this._slider.limit(limit).reInit();
474 };
475 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000476 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000477 return this._limit;
478 },
479
Nils Diewald7148c6f2015-05-04 15:07:53 +0000480
Nils Diewald86dad5b2015-01-28 15:09:07 +0000481 /**
482 * Upgrade this object to another object,
483 * while private data stays intact.
484 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000485 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000486 */
487 upgradeTo : function (props) {
488 for (var prop in props) {
Akrone4961b12017-05-10 21:04:46 +0200489 this[prop] = props[prop];
Nils Diewald86dad5b2015-01-28 15:09:07 +0000490 };
491 return this;
492 },
493
Nils Diewald7148c6f2015-05-04 15:07:53 +0000494
Nils Diewald86dad5b2015-01-28 15:09:07 +0000495 /**
Akron97752a72016-05-25 14:43:07 +0200496 * Filter the list and make it visible.
497 * This is always called once the prefix changes.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000498 *
499 * @param {string} Prefix for filtering the list
500 */
Akron6ed13992016-05-23 18:06:05 +0200501 show : function (active) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000502
Akron5240b8c2016-05-20 09:17:41 +0200503 // show menu based on initial offset
Akron6ac58442016-05-24 16:52:29 +0200504 this._unmark(); // Unmark everything that was marked before
Akrona92fd8d2016-05-24 21:13:41 +0200505 this.removeItems();
Akron6ed13992016-05-23 18:06:05 +0200506
507 // Initialize the list
508 if (!this._initList()) {
Akron6ac58442016-05-24 16:52:29 +0200509
Akrone4961b12017-05-10 21:04:46 +0200510 // The prefix is not active
511 this._prefix.active(true);
Akron6ed13992016-05-23 18:06:05 +0200512
Akrone4961b12017-05-10 21:04:46 +0200513 // finally show the element
514 this._element.classList.add('visible');
515
516 return true;
Akron6ed13992016-05-23 18:06:05 +0200517 };
518
519 var offset = 0;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000520
Akron9c2f9382016-05-25 16:36:04 +0200521 // Set a chosen value to active and move the viewport
Akron6ed13992016-05-23 18:06:05 +0200522 if (arguments.length === 1) {
523
Akrone4961b12017-05-10 21:04:46 +0200524 // Normalize active value
525 if (active < 0) {
526 active = 0;
527 }
528 else if (active >= this.liveLength()) {
529 active = this.liveLength() - 1;
530 };
Akron6ed13992016-05-23 18:06:05 +0200531
Akrone4961b12017-05-10 21:04:46 +0200532 // Item is outside the first viewport
533 if (active >= this._limit) {
534 offset = active;
535 if (offset > (this.liveLength() - this._limit)) {
536 offset = this.liveLength() - this._limit;
537 };
538 };
539
540 this.position = active;
Akron6ed13992016-05-23 18:06:05 +0200541 }
542
Akron9c2f9382016-05-25 16:36:04 +0200543 // Choose the first item
Akron6ed13992016-05-23 18:06:05 +0200544 else if (this._firstActive) {
Akrone4961b12017-05-10 21:04:46 +0200545 this.position = 0;
Akron47c086c2016-05-18 21:22:06 +0200546 }
Akroncb351d62016-05-19 23:10:33 +0200547
Akron9c2f9382016-05-25 16:36:04 +0200548 // Choose no item
Akron47c086c2016-05-18 21:22:06 +0200549 else {
Akrone4961b12017-05-10 21:04:46 +0200550 this.position = -1;
Akron6ed13992016-05-23 18:06:05 +0200551 };
552
Akron9c4d1ae2016-05-25 21:43:22 +0200553 this.offset = offset;
Akron6ed13992016-05-23 18:06:05 +0200554 this._showItems(offset); // Show new item list
555
556 // Make chosen value active
557 if (this.position !== -1) {
Akrone4961b12017-05-10 21:04:46 +0200558 this.liveItem(this.position).active(true);
Akron6ed13992016-05-23 18:06:05 +0200559 };
Akron37513a62015-11-17 01:07:11 +0100560
Akron5240b8c2016-05-20 09:17:41 +0200561 // The prefix is not active
Nils Diewalde8518f82015-03-18 22:41:49 +0000562 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000563
Akron5240b8c2016-05-20 09:17:41 +0200564 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200565 this._element.classList.add('visible');
Nils Diewald2fe12e12015-03-06 16:47:06 +0000566
Nils Diewald86dad5b2015-01-28 15:09:07 +0000567 // Add classes for rolling menus
568 this._boundary(true);
Akron6bb71582016-06-10 20:41:08 +0200569
Nils Diewald59c02fc2015-03-07 01:29:09 +0000570 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000571 },
572
Nils Diewald7148c6f2015-05-04 15:07:53 +0000573
574 /**
575 * Hide the menu and call the onHide callback.
576 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000577 hide : function () {
hebastaf95226b2019-09-19 11:37:00 +0200578 if(!this.dontHide){
579 this.removeItems();
580 this._prefix.clear();
581 this.onHide();
582 this._element.classList.remove('visible');
583 }
584 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000585 },
586
Nils Diewald7148c6f2015-05-04 15:07:53 +0000587 /**
588 * Function released when the menu hides.
589 * This method is expected to be overridden.
590 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000591 onHide : function () {},
592
Nils Diewald86dad5b2015-01-28 15:09:07 +0000593 /**
594 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000595 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000596 */
Nils Diewald5975d702015-03-09 17:45:42 +0000597 prefix : function (pref) {
598 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200599 this._prefix.value(pref);
600 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000601 };
602 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000603 },
604
Akronc7448732016-04-27 14:06:58 +0200605 /**
606 * Get the lengthField object.
607 */
608 lengthField : function () {
609 return this._lengthField;
610 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000611
Akron5240b8c2016-05-20 09:17:41 +0200612 /**
613 * Get the associated slider object.
614 */
615 slider : function () {
616 return this._slider;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000617 },
618
Akron5240b8c2016-05-20 09:17:41 +0200619
Nils Diewald86dad5b2015-01-28 15:09:07 +0000620 /**
621 * Delete all visible items from the menu element
622 */
Akrona92fd8d2016-05-24 21:13:41 +0200623 removeItems : function () {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000624 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000625
Nils Diewald2fe12e12015-03-06 16:47:06 +0000626 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000627 var children = this._element.childNodes;
Akronc7448732016-04-27 14:06:58 +0200628 // Leave the prefix and lengthField
Akron9905e2a2016-05-10 16:06:44 +0200629 for (var i = children.length - 1; i >= 3; i--) {
Akrone4961b12017-05-10 21:04:46 +0200630 this._element.removeChild(
631 children[i]
632 );
Nils Diewald5975d702015-03-09 17:45:42 +0000633 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000634 },
635
Nils Diewald2fe12e12015-03-06 16:47:06 +0000636 /**
637 * Get a specific item from the complete list
638 *
639 * @param {number} index of the list item
640 */
641 item : function (index) {
642 return this._items[index]
643 },
644
645
Nils Diewald86dad5b2015-01-28 15:09:07 +0000646 /**
647 * Get a specific item from the filtered list
648 *
649 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000650 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000651 */
652 liveItem : function (index) {
653 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200654 if (!this._initList())
655 return;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000656
657 return this._items[this._list[index]];
658 },
659
Nils Diewald86dad5b2015-01-28 15:09:07 +0000660
661 /**
Akron5240b8c2016-05-20 09:17:41 +0200662 * Get a specific item from the viewport list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000663 *
664 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000665 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000666 */
667 shownItem : function (index) {
668 if (index >= this.limit())
Akrone4961b12017-05-10 21:04:46 +0200669 return;
Akron9c4d1ae2016-05-25 21:43:22 +0200670 return this.liveItem(this.offset + index);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000671 },
672
673
Nils Diewald2fe12e12015-03-06 16:47:06 +0000674 /**
Akron9c4d1ae2016-05-25 21:43:22 +0200675 * Get the length of the full item list
Nils Diewald2fe12e12015-03-06 16:47:06 +0000676 */
677 length : function () {
678 return this._items.length;
679 },
680
681
682 /**
Akron5240b8c2016-05-20 09:17:41 +0200683 * Length of the filtered item list.
684 */
685 liveLength : function () {
686 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200687 this._initList();
Akron5240b8c2016-05-20 09:17:41 +0200688 return this._list.length;
689 },
690
Akrone817b882018-08-31 14:09:17 +0200691
Akron5240b8c2016-05-20 09:17:41 +0200692 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000693 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000694 */
695 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000696
Akronb38afb22016-05-25 19:30:01 +0200697 // No list
698 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200699 return;
Akronb38afb22016-05-25 19:30:01 +0200700
Akron9c4d1ae2016-05-25 21:43:22 +0200701 // Deactivate old item
702 if (this.position !== -1 && !this._prefix.active()) {
Akrone4961b12017-05-10 21:04:46 +0200703 this.liveItem(this.position).active(false);
Akron9c4d1ae2016-05-25 21:43:22 +0200704 };
Nils Diewalde8518f82015-03-18 22:41:49 +0000705
Akron9c4d1ae2016-05-25 21:43:22 +0200706 // Get new active item
707 this.position++;
708 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000709
Nils Diewald5975d702015-03-09 17:45:42 +0000710 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000711 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000712
Akrone4961b12017-05-10 21:04:46 +0200713 // Activate prefix
714 var prefix = this._prefix;
Nils Diewald5975d702015-03-09 17:45:42 +0000715
Akrone4961b12017-05-10 21:04:46 +0200716 // Prefix is set and not active - choose!
717 if (prefix.isSet() && !prefix.active()) {
718 this.position--;
719 prefix.active(true);
720 return;
721 }
Akron9c4d1ae2016-05-25 21:43:22 +0200722
Akrone4961b12017-05-10 21:04:46 +0200723 // Choose first item
724 else {
725 newItem = this.liveItem(0);
726 // choose first item
727 this.position = 0;
728 this._showItems(0);
729 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000730 }
731
Akron5a1f5bb2016-05-23 22:00:39 +0200732 // The next element is after the viewport - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200733 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200734 this.screen(this.position - this.limit() + 1);
Akron5a1f5bb2016-05-23 22:00:39 +0200735 }
736
737 // The next element is before the viewport - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200738 else if (this.position <= this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200739 this.screen(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000740 };
Nils Diewald5975d702015-03-09 17:45:42 +0000741
742 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000743 newItem.active(true);
744 },
745
Nils Diewalde8518f82015-03-18 22:41:49 +0000746 /*
Nils Diewald86dad5b2015-01-28 15:09:07 +0000747 * Make the previous item in the menu active
748 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000749 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +0000750
Akronb38afb22016-05-25 19:30:01 +0200751 // No list
752 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200753 return;
Akronb38afb22016-05-25 19:30:01 +0200754
Akron9c4d1ae2016-05-25 21:43:22 +0200755 // Deactivate old item
Nils Diewald2d210752015-03-09 19:01:15 +0000756 if (!this._prefix.active()) {
Akron9c4d1ae2016-05-25 21:43:22 +0200757
Akrone4961b12017-05-10 21:04:46 +0200758 // No active element set
759 if (this.position === -1) {
760 this.position = this.liveLength();
761 }
Akron9c4d1ae2016-05-25 21:43:22 +0200762
Akrone4961b12017-05-10 21:04:46 +0200763 // No active element set
764 else {
765 this.liveItem(this.position--).active(false);
766 };
Nils Diewald2d210752015-03-09 19:01:15 +0000767 };
768
Akron9c4d1ae2016-05-25 21:43:22 +0200769 // Get new active item
770 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000771
772 // The previous element is undefined - roll to bottom
773 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000774
Akrone4961b12017-05-10 21:04:46 +0200775 // Activate prefix
776 var prefix = this._prefix;
777 var offset = this.liveLength() - this.limit();
778
779 // Normalize offset
780 offset = offset < 0 ? 0 : offset;
Nils Diewald2d210752015-03-09 19:01:15 +0000781
Akrone4961b12017-05-10 21:04:46 +0200782 // Choose the last item
783 this.position = this.liveLength() - 1;
784
785 // Prefix is set and not active - choose!
786 if (prefix.isSet() && !prefix.active()) {
787 this.position++;
788 prefix.active(true);
789 this.offset = offset;
790 return;
791 }
Nils Diewald2d210752015-03-09 19:01:15 +0000792
Akrone4961b12017-05-10 21:04:46 +0200793 // Choose last item
794 else {
795 newItem = this.liveItem(this.position);
796 this._showItems(offset);
797 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000798 }
799
Akron5a1f5bb2016-05-23 22:00:39 +0200800 // The previous element is before the view - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200801 else if (this.position < this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200802 this.screen(this.position);
Akron5a1f5bb2016-05-23 22:00:39 +0200803 }
804
805 // The previous element is after the view - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200806 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200807 this.screen(this.position - this.limit() + 2);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000808 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000809
Nils Diewald5975d702015-03-09 17:45:42 +0000810 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000811 newItem.active(true);
812 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000813
Akron3c2730f2016-05-24 15:08:29 +0200814 /**
815 * Move the page up by limit!
816 */
817 pageUp : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200818 this.screen(this.offset - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200819 },
820
821
822 /**
823 * Move the page down by limit!
824 */
825 pageDown : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200826 this.screen(this.offset + this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200827 },
828
Nils Diewald86dad5b2015-01-28 15:09:07 +0000829
Akrone817b882018-08-31 14:09:17 +0200830 /**
831 * Move the view one item up
832 */
833 viewUp : function () {
834 this.screen(this.offset - 1);
835 },
836
837
838 /**
839 * Move the view one item down
840 */
841 viewDown : function () {
842 this.screen(this.offset + 1);
843 },
844
Akron5240b8c2016-05-20 09:17:41 +0200845 // Unmark all items
846 _unmark : function () {
Akron678c26f2020-10-09 08:52:50 +0200847 this._list.forEach(function(it){
848 var item = this._items[it];
Akrone4961b12017-05-10 21:04:46 +0200849 item.lowlight();
Akron678c26f2020-10-09 08:52:50 +0200850 item.active(false);
851 }, this);
Akron5240b8c2016-05-20 09:17:41 +0200852 },
853
Akron5240b8c2016-05-20 09:17:41 +0200854 // Set boundary for viewport
855 _boundary : function (bool) {
Akron55a343b2018-04-06 19:57:36 +0200856 if (this._list.length === 0)
857 return;
Akron5240b8c2016-05-20 09:17:41 +0200858 this.item(this._list[0]).noMore(bool);
859 this.item(this._list[this._list.length - 1]).noMore(bool);
860 },
861
862
863 // Append Items that should be shown
864 _showItems : function (off) {
865
Akrona92fd8d2016-05-24 21:13:41 +0200866 // optimization: scroll down one step
Akron9c4d1ae2016-05-25 21:43:22 +0200867 if (this.offset === (off - 1)) {
Akrone4961b12017-05-10 21:04:46 +0200868 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200869
Akrone4961b12017-05-10 21:04:46 +0200870 // Remove the HTML node from the first item
871 // leave lengthField/prefix/slider
872 this._element.removeChild(this._element.children[3]);
873 var pos = this.offset + this.limit() - 1;
874 this._append(this._list[pos]);
Akrona92fd8d2016-05-24 21:13:41 +0200875 }
Akron5240b8c2016-05-20 09:17:41 +0200876
Akrona92fd8d2016-05-24 21:13:41 +0200877 // optimization: scroll up one step
Akron9c4d1ae2016-05-25 21:43:22 +0200878 else if (this.offset === (off + 1)) {
Akrone4961b12017-05-10 21:04:46 +0200879 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200880
Akrone4961b12017-05-10 21:04:46 +0200881 // Remove the HTML node from the last item
882 this._element.removeChild(this._element.lastChild);
Akron9c4d1ae2016-05-25 21:43:22 +0200883
Akrone4961b12017-05-10 21:04:46 +0200884 this._prepend(this._list[this.offset]);
Akrona92fd8d2016-05-24 21:13:41 +0200885 }
886 else {
Akrone4961b12017-05-10 21:04:46 +0200887 this.offset = off;
Akron5240b8c2016-05-20 09:17:41 +0200888
Akrone4961b12017-05-10 21:04:46 +0200889 // Remove all items
890 this.removeItems();
Akron5240b8c2016-05-20 09:17:41 +0200891
Akrone4961b12017-05-10 21:04:46 +0200892 // Use list
893 var shown = 0;
894 var i;
Akron5240b8c2016-05-20 09:17:41 +0200895
Akron678c26f2020-10-09 08:52:50 +0200896 for (let i = 0; i < this._list.length; i++) {
Akrona92fd8d2016-05-24 21:13:41 +0200897
Akrone4961b12017-05-10 21:04:46 +0200898 // Don't show - it's before offset
899 shown++;
900 if (shown <= off)
901 continue;
Akrona92fd8d2016-05-24 21:13:41 +0200902
Akrone4961b12017-05-10 21:04:46 +0200903 var itemNr = this._list[i];
904 var item = this.item(itemNr);
905 this._append(itemNr);
906
907 if (shown >= (this.limit() + off))
908 break;
909 };
Akron5240b8c2016-05-20 09:17:41 +0200910 };
911
912 // set the slider to the new offset
Akron9c4d1ae2016-05-25 21:43:22 +0200913 this._slider.offset(this.offset);
Akron5240b8c2016-05-20 09:17:41 +0200914 },
915
916
917 // Append item to the shown list based on index
918 _append : function (i) {
919 var item = this.item(i);
920
921 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200922 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200923 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200924 };
925
Akron5240b8c2016-05-20 09:17:41 +0200926
927 // Append element
928 this.element().appendChild(item.element());
929 },
930
931
932 // Prepend item to the shown list based on index
933 _prepend : function (i) {
934 var item = this.item(i);
935
936 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200937 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200938 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200939 };
Akron5240b8c2016-05-20 09:17:41 +0200940
941 var e = this.element();
Akron9c4d1ae2016-05-25 21:43:22 +0200942
Akron5240b8c2016-05-20 09:17:41 +0200943 // Append element after lengthField/prefix/slider
944 e.insertBefore(
Akrone4961b12017-05-10 21:04:46 +0200945 item.element(),
946 e.children[3]
Akron5240b8c2016-05-20 09:17:41 +0200947 );
Nils Diewald2fe12e12015-03-06 16:47:06 +0000948 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000949 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000950});