blob: 1fb1ffdfb83f39f7880a551cdf55c53babf2b0d1 [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
153 return this;
154 },
155
156 // Read items to add to list
157 readItems : function (list) {
158
159 this._list = undefined;
160
161 // Remove circular reference to "this" in items
162 for (var i = 0; i < this._items.length; i++) {
163 delete this._items[i]["_menu"];
164 delete this._items[i];
165 };
166
167 this._items = new Array();
168 this.removeItems();
169
170
171 // Initialize items
172 this._lengthField.reset();
173
Akron9c4d1ae2016-05-25 21:43:22 +0200174 var i = 0;
Akron5240b8c2016-05-20 09:17:41 +0200175 // Initialize item list based on parameters
Akron7524be12016-06-01 17:31:33 +0200176 for (i in list) {
Akrone4961b12017-05-10 21:04:46 +0200177 var obj = this._itemClass.create(list[i]);
Akron5240b8c2016-05-20 09:17:41 +0200178
Akrone4961b12017-05-10 21:04:46 +0200179 // This may become circular
180 obj["_menu"] = this;
181 this._lengthField.add(list[i]);
182 this._items.push(obj);
Akron5240b8c2016-05-20 09:17:41 +0200183 };
184
Akron9c4d1ae2016-05-25 21:43:22 +0200185 this._slider.length(this.liveLength())
Akrone4961b12017-05-10 21:04:46 +0200186 .limit(this._limit)
187 .reInit();
188
Akroneaba63e2018-01-26 19:49:30 +0100189 this._firstActive = false;
190 // Show the first item active always?
Akron9c4d1ae2016-05-25 21:43:22 +0200191 this.offset = 0;
192 this.position = 0;
Akron5240b8c2016-05-20 09:17:41 +0200193 },
Akroneaba63e2018-01-26 19:49:30 +0100194
Akron5240b8c2016-05-20 09:17:41 +0200195 // Initialize the item list
196 _initList : function () {
197
198 // Create a new list
199 if (this._list === undefined) {
Akrone4961b12017-05-10 21:04:46 +0200200 this._list = [];
Akron5240b8c2016-05-20 09:17:41 +0200201 }
202 else if (this._list.length !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200203 this._boundary(false);
204 this._list.length = 0;
Akron5240b8c2016-05-20 09:17:41 +0200205 };
206
207 // Offset is initially zero
Akron9c4d1ae2016-05-25 21:43:22 +0200208 this.offset = 0;
Akron5240b8c2016-05-20 09:17:41 +0200209
210 // There is no prefix set
211 if (this.prefix().length <= 0) {
212
Akrone4961b12017-05-10 21:04:46 +0200213 // add all items to the list and lowlight
214 var i = 0;
215 for (; i < this._items.length; i++) {
216 this._list.push(i);
217 this._items[i].lowlight();
218 };
Akron5240b8c2016-05-20 09:17:41 +0200219
Akroneaba63e2018-01-26 19:49:30 +0100220 this._slider.length(i).reInit();
Akron97752a72016-05-25 14:43:07 +0200221
Akrone4961b12017-05-10 21:04:46 +0200222 return true;
Akron5240b8c2016-05-20 09:17:41 +0200223 };
224
225 /*
226 * There is a prefix set, so filter the list!
227 */
228 var pos;
Akronacffc652017-12-18 21:21:25 +0100229 var prefixList = this.prefix().toLowerCase().split(" ");
230
231 var items = [];
232 var maxPoints = 1; // minimum 1
Akron5240b8c2016-05-20 09:17:41 +0200233
234 // Iterate over all items and choose preferred matching items
235 // i.e. the matching happens at the word start
236 for (pos = 0; pos < this._items.length; pos++) {
Akronacffc652017-12-18 21:21:25 +0100237
238 var points = 0;
239
240 for (pref = 0; pref < prefixList.length; pref++) {
241 var prefix = " " + prefixList[pref];
242
243 // Check if it matches at the beginning
Akron3b253d32018-07-15 10:16:06 +0200244 // if ((this.item(pos).lcField().indexOf(prefix)) >= 0) {
245 if ((this.item(pos).lcField().includes(prefix))) {
Akronacffc652017-12-18 21:21:25 +0100246 points += 5;
247 }
248
249 // Check if it matches anywhere
Akron3b253d32018-07-15 10:16:06 +0200250 // else if ((this.item(pos).lcField().indexOf(prefix.substring(1))) >= 0) {
251 else if ((this.item(pos).lcField().includes(prefix.substring(1)))) {
Akronacffc652017-12-18 21:21:25 +0100252 points += 1;
253 };
254 };
255
256 if (points > maxPoints) {
257 this._list = [pos];
258 maxPoints = points;
259 }
260 else if (points == maxPoints) {
Akrone4961b12017-05-10 21:04:46 +0200261 this._list.push(pos);
Akronacffc652017-12-18 21:21:25 +0100262 }
Akron5240b8c2016-05-20 09:17:41 +0200263 };
264
265 // The list is empty - so lower your expectations
266 // Iterate over all items and choose matching items
267 // i.e. the matching happens anywhere in the word
Akronacffc652017-12-18 21:21:25 +0100268 /*
Akron6ffad5d2016-05-24 17:16:58 +0200269 prefix = prefix.substring(1);
Akron5240b8c2016-05-20 09:17:41 +0200270 if (this._list.length == 0) {
Akrone4961b12017-05-10 21:04:46 +0200271 for (pos = 0; pos < this._items.length; pos++) {
272 if ((this.item(pos).lcField().indexOf(prefix)) >= 0)
273 this._list.push(pos);
274 };
Akron5240b8c2016-05-20 09:17:41 +0200275 };
Akronacffc652017-12-18 21:21:25 +0100276 */
Akron5240b8c2016-05-20 09:17:41 +0200277
Akron9c4d1ae2016-05-25 21:43:22 +0200278 this._slider.length(this._list.length).reInit();
Akron6ed13992016-05-23 18:06:05 +0200279
Akron5240b8c2016-05-20 09:17:41 +0200280 // Filter was successful - yeah!
281 return this._list.length > 0 ? true : false;
282 },
283
284
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000285 /**
286 * Destroy this menu
287 * (in case you don't trust the
288 * mark and sweep GC)!
289 */
290 destroy : function () {
Akron47c086c2016-05-18 21:22:06 +0200291
Akron5240b8c2016-05-20 09:17:41 +0200292 // Remove circular reference to "this" in menu
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000293 if (this._element != undefined)
Akrone4961b12017-05-10 21:04:46 +0200294 delete this._element["menu"];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000295
Akron5240b8c2016-05-20 09:17:41 +0200296 // Remove circular reference to "this" in items
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000297 for (var i = 0; i < this._items.length; i++) {
Akrone4961b12017-05-10 21:04:46 +0200298 delete this._items[i]["_menu"];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000299 };
Akron5240b8c2016-05-20 09:17:41 +0200300
301 // Remove circular reference to "this" in prefix
Nils Diewald5c5a7472015-04-02 22:13:38 +0000302 delete this._prefix['_menu'];
Akron5240b8c2016-05-20 09:17:41 +0200303 delete this._lengthField['_menu'];
304 delete this._slider['_menu'];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000305 },
306
Nils Diewald7148c6f2015-05-04 15:07:53 +0000307
308 /**
309 * Focus on this menu.
310 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000311 focus : function () {
312 this._element.focus();
313 },
314
Nils Diewald7148c6f2015-05-04 15:07:53 +0000315
Nils Diewald59c02fc2015-03-07 01:29:09 +0000316 // mouse wheel treatment
317 _mousewheel : function (e) {
318 var delta = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000319 delta = e.deltaY / 120;
320 if (delta > 0)
Akrone4961b12017-05-10 21:04:46 +0200321 this.next();
Nils Diewald5975d702015-03-09 17:45:42 +0000322 else if (delta < 0)
Akrone4961b12017-05-10 21:04:46 +0200323 this.prev();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000324 e.halt();
325 },
326
Akrona1159ff2018-07-22 13:28:31 +0200327 // touchmove treatment
328 _touch : function (e) {
329 var s = this.slider();
330 if (e.type === "touchstart") {
331 // s.active(true);
332 var t = e.touches[0];
333 this._lastTouch = t.clientY;
334 }
335 else if (e.type === "touchend") {
336 // s.active(false);
337 this._lastTouch = undefined;
Akrona1159ff2018-07-22 13:28:31 +0200338 }
339 else if (e.type === "touchmove") {
340 var t = e.touches[0];
Akrone817b882018-08-31 14:09:17 +0200341
342 // TODO:
343 // Instead of using 26px, choose the item height
344 // or use the menu height // shownItems
345
Akrona1159ff2018-07-22 13:28:31 +0200346 // s.movetoRel(t.clientY - this._initTouch);
347 if ((this._lastTouch + 26) < t.clientY) {
Akrone817b882018-08-31 14:09:17 +0200348 this.viewDown();
Akrona1159ff2018-07-22 13:28:31 +0200349 this._lastTouch = t.clientY;
350 }
351 else if ((this._lastTouch - 26) > t.clientY) {
Akrone817b882018-08-31 14:09:17 +0200352 this.viewUp();
Akrona1159ff2018-07-22 13:28:31 +0200353 this._lastTouch = t.clientY;
354 }
355 e.halt();
356 };
357 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000358
Nils Diewald59c02fc2015-03-07 01:29:09 +0000359 // Arrow key and prefix treatment
Nils Diewald47f366b2015-04-15 20:06:35 +0000360 _keydown : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000361 var code = _codeFromEvent(e);
362
Nils Diewald59c02fc2015-03-07 01:29:09 +0000363 switch (code) {
364 case 27: // 'Esc'
Akrone4961b12017-05-10 21:04:46 +0200365 e.halt();
366 this.hide();
367 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000368
Nils Diewald59c02fc2015-03-07 01:29:09 +0000369 case 38: // 'Up'
Akrone4961b12017-05-10 21:04:46 +0200370 e.halt();
371 this.prev();
372 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000373 case 33: // 'Page up'
Akrone4961b12017-05-10 21:04:46 +0200374 e.halt();
375 this.pageUp();
376 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000377 case 40: // 'Down'
Akrone4961b12017-05-10 21:04:46 +0200378 e.halt();
379 this.next();
380 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000381 case 34: // 'Page down'
Akrone4961b12017-05-10 21:04:46 +0200382 e.halt();
383 this.pageDown();
384 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000385 case 39: // 'Right'
Akrone4961b12017-05-10 21:04:46 +0200386 if (this._prefix.active())
387 break;
Nils Diewalde8518f82015-03-18 22:41:49 +0000388
Akrone4961b12017-05-10 21:04:46 +0200389 var item = this.liveItem(this.position);
390
391 if (item["further"] !== undefined) {
392 item["further"].bind(item).apply();
393 };
394
395 e.halt();
396 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000397 case 13: // 'Enter'
398
Akrone4961b12017-05-10 21:04:46 +0200399 // Click on prefix
400 if (this._prefix.active())
401 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000402
Akrone4961b12017-05-10 21:04:46 +0200403 // Click on item
404 else
405 this.liveItem(this.position).onclick(e);
406 e.halt();
407 break;
Nils Diewald59c02fc2015-03-07 01:29:09 +0000408 case 8: // 'Backspace'
Akrone4961b12017-05-10 21:04:46 +0200409 this._prefix.chop();
410 this.show();
411 e.halt();
412 break;
Nils Diewald47f366b2015-04-15 20:06:35 +0000413 };
414 },
Nils Diewald59c02fc2015-03-07 01:29:09 +0000415
Nils Diewald47f366b2015-04-15 20:06:35 +0000416 // Add characters to prefix
417 _keypress : function (e) {
Akron9c2f9382016-05-25 16:36:04 +0200418 if (e.charCode !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200419 e.halt();
420 var c = String.fromCharCode(_codeFromEvent(e));
421
422 // Add prefix
423 this._prefix.add(c);
424 this.show();
Akron9c2f9382016-05-25 16:36:04 +0200425 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000426 },
427
Akron47c086c2016-05-18 21:22:06 +0200428 /**
Akron5240b8c2016-05-20 09:17:41 +0200429 * Show a screen with a given offset
430 * in the viewport.
Akron47c086c2016-05-18 21:22:06 +0200431 */
432 screen : function (nr) {
Akrone817b882018-08-31 14:09:17 +0200433
434 // Normalize negative values
Akron3c2730f2016-05-24 15:08:29 +0200435 if (nr < 0) {
Akrone4961b12017-05-10 21:04:46 +0200436 nr = 0
Akron3c2730f2016-05-24 15:08:29 +0200437 }
Akrone817b882018-08-31 14:09:17 +0200438
439 // The shown list already shows everything
440 else if (this.liveLength() < this.limit()) {
441 return false;
442 }
443
444 // Move relatively to the next screen
Akron6ac58442016-05-24 16:52:29 +0200445 else if (nr > (this.liveLength() - this.limit())) {
Akrone4961b12017-05-10 21:04:46 +0200446 nr = (this.liveLength() - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200447 };
448
Akrone817b882018-08-31 14:09:17 +0200449 // no change
Akron9c4d1ae2016-05-25 21:43:22 +0200450 if (this.offset === nr)
Akrone817b882018-08-31 14:09:17 +0200451 return false;
Akron5a1f5bb2016-05-23 22:00:39 +0200452
Akron47c086c2016-05-18 21:22:06 +0200453 this._showItems(nr);
Akrone817b882018-08-31 14:09:17 +0200454
455 return true;
Akron47c086c2016-05-18 21:22:06 +0200456 },
457
Akrone817b882018-08-31 14:09:17 +0200458
Nils Diewald2fe12e12015-03-06 16:47:06 +0000459 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000460 * Get the associated dom element.
Nils Diewald2fe12e12015-03-06 16:47:06 +0000461 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000462 element : function () {
463 return this._element;
464 },
465
Nils Diewald2fe12e12015-03-06 16:47:06 +0000466 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000467 * Get the creator class for items
Nils Diewald2fe12e12015-03-06 16:47:06 +0000468 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000469 itemClass : function () {
470 return this._itemClass;
471 },
472
473 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000474 * Get and set the numerical value
475 * for the maximum number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000476 */
477 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000478 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200479 if (this._limit !== limit) {
480 this._limit = limit;
481 this._slider.limit(limit).reInit();
482 };
483 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000484 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000485 return this._limit;
486 },
487
Nils Diewald7148c6f2015-05-04 15:07:53 +0000488
Nils Diewald86dad5b2015-01-28 15:09:07 +0000489 /**
490 * Upgrade this object to another object,
491 * while private data stays intact.
492 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000493 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000494 */
495 upgradeTo : function (props) {
496 for (var prop in props) {
Akrone4961b12017-05-10 21:04:46 +0200497 this[prop] = props[prop];
Nils Diewald86dad5b2015-01-28 15:09:07 +0000498 };
499 return this;
500 },
501
Nils Diewald7148c6f2015-05-04 15:07:53 +0000502
Nils Diewald86dad5b2015-01-28 15:09:07 +0000503 /**
Akron97752a72016-05-25 14:43:07 +0200504 * Filter the list and make it visible.
505 * This is always called once the prefix changes.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000506 *
507 * @param {string} Prefix for filtering the list
508 */
Akron6ed13992016-05-23 18:06:05 +0200509 show : function (active) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000510
Akron5240b8c2016-05-20 09:17:41 +0200511 // show menu based on initial offset
Akron6ac58442016-05-24 16:52:29 +0200512 this._unmark(); // Unmark everything that was marked before
Akrona92fd8d2016-05-24 21:13:41 +0200513 this.removeItems();
Akron6ed13992016-05-23 18:06:05 +0200514
515 // Initialize the list
516 if (!this._initList()) {
Akron6ac58442016-05-24 16:52:29 +0200517
Akrone4961b12017-05-10 21:04:46 +0200518 // The prefix is not active
519 this._prefix.active(true);
Akron6ed13992016-05-23 18:06:05 +0200520
Akrone4961b12017-05-10 21:04:46 +0200521 // finally show the element
522 this._element.classList.add('visible');
523
524 return true;
Akron6ed13992016-05-23 18:06:05 +0200525 };
526
527 var offset = 0;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000528
Akron9c2f9382016-05-25 16:36:04 +0200529 // Set a chosen value to active and move the viewport
Akron6ed13992016-05-23 18:06:05 +0200530 if (arguments.length === 1) {
531
Akrone4961b12017-05-10 21:04:46 +0200532 // Normalize active value
533 if (active < 0) {
534 active = 0;
535 }
536 else if (active >= this.liveLength()) {
537 active = this.liveLength() - 1;
538 };
Akron6ed13992016-05-23 18:06:05 +0200539
Akrone4961b12017-05-10 21:04:46 +0200540 // Item is outside the first viewport
541 if (active >= this._limit) {
542 offset = active;
543 if (offset > (this.liveLength() - this._limit)) {
544 offset = this.liveLength() - this._limit;
545 };
546 };
547
548 this.position = active;
Akron6ed13992016-05-23 18:06:05 +0200549 }
550
Akron9c2f9382016-05-25 16:36:04 +0200551 // Choose the first item
Akron6ed13992016-05-23 18:06:05 +0200552 else if (this._firstActive) {
Akrone4961b12017-05-10 21:04:46 +0200553 this.position = 0;
Akron47c086c2016-05-18 21:22:06 +0200554 }
Akroncb351d62016-05-19 23:10:33 +0200555
Akron9c2f9382016-05-25 16:36:04 +0200556 // Choose no item
Akron47c086c2016-05-18 21:22:06 +0200557 else {
Akrone4961b12017-05-10 21:04:46 +0200558 this.position = -1;
Akron6ed13992016-05-23 18:06:05 +0200559 };
560
Akron9c4d1ae2016-05-25 21:43:22 +0200561 this.offset = offset;
Akron6ed13992016-05-23 18:06:05 +0200562 this._showItems(offset); // Show new item list
563
564 // Make chosen value active
565 if (this.position !== -1) {
Akrone4961b12017-05-10 21:04:46 +0200566 this.liveItem(this.position).active(true);
Akron6ed13992016-05-23 18:06:05 +0200567 };
Akron37513a62015-11-17 01:07:11 +0100568
Akron5240b8c2016-05-20 09:17:41 +0200569 // The prefix is not active
Nils Diewalde8518f82015-03-18 22:41:49 +0000570 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000571
Akron5240b8c2016-05-20 09:17:41 +0200572 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200573 this._element.classList.add('visible');
Nils Diewald2fe12e12015-03-06 16:47:06 +0000574
Nils Diewald86dad5b2015-01-28 15:09:07 +0000575 // Add classes for rolling menus
576 this._boundary(true);
Akron6bb71582016-06-10 20:41:08 +0200577
Nils Diewald59c02fc2015-03-07 01:29:09 +0000578 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000579 },
580
Nils Diewald7148c6f2015-05-04 15:07:53 +0000581
582 /**
583 * Hide the menu and call the onHide callback.
584 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000585 hide : function () {
Akrona92fd8d2016-05-24 21:13:41 +0200586 this.removeItems();
Nils Diewald7148c6f2015-05-04 15:07:53 +0000587 this._prefix.clear();
Nils Diewald5c5a7472015-04-02 22:13:38 +0000588 this.onHide();
Akron6bb71582016-06-10 20:41:08 +0200589 this._element.classList.remove('visible');
590
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000591 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000592 },
593
Nils Diewald7148c6f2015-05-04 15:07:53 +0000594 /**
595 * Function released when the menu hides.
596 * This method is expected to be overridden.
597 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000598 onHide : function () {},
599
Nils Diewald86dad5b2015-01-28 15:09:07 +0000600 /**
601 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000602 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000603 */
Nils Diewald5975d702015-03-09 17:45:42 +0000604 prefix : function (pref) {
605 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200606 this._prefix.value(pref);
607 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000608 };
609 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000610 },
611
Akronc7448732016-04-27 14:06:58 +0200612 /**
613 * Get the lengthField object.
614 */
615 lengthField : function () {
616 return this._lengthField;
617 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000618
Akron5240b8c2016-05-20 09:17:41 +0200619 /**
620 * Get the associated slider object.
621 */
622 slider : function () {
623 return this._slider;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000624 },
625
Akron5240b8c2016-05-20 09:17:41 +0200626
Nils Diewald86dad5b2015-01-28 15:09:07 +0000627 /**
628 * Delete all visible items from the menu element
629 */
Akrona92fd8d2016-05-24 21:13:41 +0200630 removeItems : function () {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000631 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000632
Nils Diewald2fe12e12015-03-06 16:47:06 +0000633 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000634 var children = this._element.childNodes;
Akronc7448732016-04-27 14:06:58 +0200635 // Leave the prefix and lengthField
Akron9905e2a2016-05-10 16:06:44 +0200636 for (var i = children.length - 1; i >= 3; i--) {
Akrone4961b12017-05-10 21:04:46 +0200637 this._element.removeChild(
638 children[i]
639 );
Nils Diewald5975d702015-03-09 17:45:42 +0000640 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000641 },
642
Nils Diewald2fe12e12015-03-06 16:47:06 +0000643 /**
644 * Get a specific item from the complete list
645 *
646 * @param {number} index of the list item
647 */
648 item : function (index) {
649 return this._items[index]
650 },
651
652
Nils Diewald86dad5b2015-01-28 15:09:07 +0000653 /**
654 * Get a specific item from the filtered list
655 *
656 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000657 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000658 */
659 liveItem : function (index) {
660 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200661 if (!this._initList())
662 return;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000663
664 return this._items[this._list[index]];
665 },
666
Nils Diewald86dad5b2015-01-28 15:09:07 +0000667
668 /**
Akron5240b8c2016-05-20 09:17:41 +0200669 * Get a specific item from the viewport list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000670 *
671 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000672 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000673 */
674 shownItem : function (index) {
675 if (index >= this.limit())
Akrone4961b12017-05-10 21:04:46 +0200676 return;
Akron9c4d1ae2016-05-25 21:43:22 +0200677 return this.liveItem(this.offset + index);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000678 },
679
680
Nils Diewald2fe12e12015-03-06 16:47:06 +0000681 /**
Akron9c4d1ae2016-05-25 21:43:22 +0200682 * Get the length of the full item list
Nils Diewald2fe12e12015-03-06 16:47:06 +0000683 */
684 length : function () {
685 return this._items.length;
686 },
687
688
689 /**
Akron5240b8c2016-05-20 09:17:41 +0200690 * Length of the filtered item list.
691 */
692 liveLength : function () {
693 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200694 this._initList();
Akron5240b8c2016-05-20 09:17:41 +0200695 return this._list.length;
696 },
697
Akrone817b882018-08-31 14:09:17 +0200698
Akron5240b8c2016-05-20 09:17:41 +0200699 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000700 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000701 */
702 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000703
Akronb38afb22016-05-25 19:30:01 +0200704 // No list
705 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200706 return;
Akronb38afb22016-05-25 19:30:01 +0200707
Akron9c4d1ae2016-05-25 21:43:22 +0200708 // Deactivate old item
709 if (this.position !== -1 && !this._prefix.active()) {
Akrone4961b12017-05-10 21:04:46 +0200710 this.liveItem(this.position).active(false);
Akron9c4d1ae2016-05-25 21:43:22 +0200711 };
Nils Diewalde8518f82015-03-18 22:41:49 +0000712
Akron9c4d1ae2016-05-25 21:43:22 +0200713 // Get new active item
714 this.position++;
715 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000716
Nils Diewald5975d702015-03-09 17:45:42 +0000717 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000718 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000719
Akrone4961b12017-05-10 21:04:46 +0200720 // Activate prefix
721 var prefix = this._prefix;
Nils Diewald5975d702015-03-09 17:45:42 +0000722
Akrone4961b12017-05-10 21:04:46 +0200723 // Prefix is set and not active - choose!
724 if (prefix.isSet() && !prefix.active()) {
725 this.position--;
726 prefix.active(true);
727 return;
728 }
Akron9c4d1ae2016-05-25 21:43:22 +0200729
Akrone4961b12017-05-10 21:04:46 +0200730 // Choose first item
731 else {
732 newItem = this.liveItem(0);
733 // choose first item
734 this.position = 0;
735 this._showItems(0);
736 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000737 }
738
Akron5a1f5bb2016-05-23 22:00:39 +0200739 // The next element is after the viewport - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200740 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200741 this.screen(this.position - this.limit() + 1);
Akron5a1f5bb2016-05-23 22:00:39 +0200742 }
743
744 // The next element is before the viewport - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200745 else if (this.position <= this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200746 this.screen(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000747 };
Nils Diewald5975d702015-03-09 17:45:42 +0000748
749 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000750 newItem.active(true);
751 },
752
Nils Diewalde8518f82015-03-18 22:41:49 +0000753 /*
Nils Diewald86dad5b2015-01-28 15:09:07 +0000754 * Make the previous item in the menu active
755 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000756 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +0000757
Akronb38afb22016-05-25 19:30:01 +0200758 // No list
759 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200760 return;
Akronb38afb22016-05-25 19:30:01 +0200761
Akron9c4d1ae2016-05-25 21:43:22 +0200762 // Deactivate old item
Nils Diewald2d210752015-03-09 19:01:15 +0000763 if (!this._prefix.active()) {
Akron9c4d1ae2016-05-25 21:43:22 +0200764
Akrone4961b12017-05-10 21:04:46 +0200765 // No active element set
766 if (this.position === -1) {
767 this.position = this.liveLength();
768 }
Akron9c4d1ae2016-05-25 21:43:22 +0200769
Akrone4961b12017-05-10 21:04:46 +0200770 // No active element set
771 else {
772 this.liveItem(this.position--).active(false);
773 };
Nils Diewald2d210752015-03-09 19:01:15 +0000774 };
775
Akron9c4d1ae2016-05-25 21:43:22 +0200776 // Get new active item
777 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000778
779 // The previous element is undefined - roll to bottom
780 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000781
Akrone4961b12017-05-10 21:04:46 +0200782 // Activate prefix
783 var prefix = this._prefix;
784 var offset = this.liveLength() - this.limit();
785
786 // Normalize offset
787 offset = offset < 0 ? 0 : offset;
Nils Diewald2d210752015-03-09 19:01:15 +0000788
Akrone4961b12017-05-10 21:04:46 +0200789 // Choose the last item
790 this.position = this.liveLength() - 1;
791
792 // Prefix is set and not active - choose!
793 if (prefix.isSet() && !prefix.active()) {
794 this.position++;
795 prefix.active(true);
796 this.offset = offset;
797 return;
798 }
Nils Diewald2d210752015-03-09 19:01:15 +0000799
Akrone4961b12017-05-10 21:04:46 +0200800 // Choose last item
801 else {
802 newItem = this.liveItem(this.position);
803 this._showItems(offset);
804 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000805 }
806
Akron5a1f5bb2016-05-23 22:00:39 +0200807 // The previous element is before the view - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200808 else if (this.position < this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200809 this.screen(this.position);
Akron5a1f5bb2016-05-23 22:00:39 +0200810 }
811
812 // The previous element is after the view - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200813 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200814 this.screen(this.position - this.limit() + 2);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000815 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000816
Nils Diewald5975d702015-03-09 17:45:42 +0000817 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000818 newItem.active(true);
819 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000820
Akron3c2730f2016-05-24 15:08:29 +0200821 /**
822 * Move the page up by limit!
823 */
824 pageUp : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200825 this.screen(this.offset - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200826 },
827
828
829 /**
830 * Move the page down by limit!
831 */
832 pageDown : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200833 this.screen(this.offset + this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200834 },
835
Nils Diewald86dad5b2015-01-28 15:09:07 +0000836
Akrone817b882018-08-31 14:09:17 +0200837 /**
838 * Move the view one item up
839 */
840 viewUp : function () {
841 this.screen(this.offset - 1);
842 },
843
844
845 /**
846 * Move the view one item down
847 */
848 viewDown : function () {
849 this.screen(this.offset + 1);
850 },
851
Akron5240b8c2016-05-20 09:17:41 +0200852 // Unmark all items
853 _unmark : function () {
854 for (var i in this._list) {
Akrone4961b12017-05-10 21:04:46 +0200855 var item = this._items[this._list[i]];
856 item.lowlight();
857 item.active(false);
Akron5240b8c2016-05-20 09:17:41 +0200858 };
859 },
860
Akron5240b8c2016-05-20 09:17:41 +0200861 // Set boundary for viewport
862 _boundary : function (bool) {
Akron55a343b2018-04-06 19:57:36 +0200863 if (this._list.length === 0)
864 return;
Akron5240b8c2016-05-20 09:17:41 +0200865 this.item(this._list[0]).noMore(bool);
866 this.item(this._list[this._list.length - 1]).noMore(bool);
867 },
868
869
870 // Append Items that should be shown
871 _showItems : function (off) {
872
Akrona92fd8d2016-05-24 21:13:41 +0200873 // optimization: scroll down one step
Akron9c4d1ae2016-05-25 21:43:22 +0200874 if (this.offset === (off - 1)) {
Akrone4961b12017-05-10 21:04:46 +0200875 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200876
Akrone4961b12017-05-10 21:04:46 +0200877 // Remove the HTML node from the first item
878 // leave lengthField/prefix/slider
879 this._element.removeChild(this._element.children[3]);
880 var pos = this.offset + this.limit() - 1;
881 this._append(this._list[pos]);
Akrona92fd8d2016-05-24 21:13:41 +0200882 }
Akron5240b8c2016-05-20 09:17:41 +0200883
Akrona92fd8d2016-05-24 21:13:41 +0200884 // optimization: scroll up one step
Akron9c4d1ae2016-05-25 21:43:22 +0200885 else if (this.offset === (off + 1)) {
Akrone4961b12017-05-10 21:04:46 +0200886 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200887
Akrone4961b12017-05-10 21:04:46 +0200888 // Remove the HTML node from the last item
889 this._element.removeChild(this._element.lastChild);
Akron9c4d1ae2016-05-25 21:43:22 +0200890
Akrone4961b12017-05-10 21:04:46 +0200891 this._prepend(this._list[this.offset]);
Akrona92fd8d2016-05-24 21:13:41 +0200892 }
893 else {
Akrone4961b12017-05-10 21:04:46 +0200894 this.offset = off;
Akron5240b8c2016-05-20 09:17:41 +0200895
Akrone4961b12017-05-10 21:04:46 +0200896 // Remove all items
897 this.removeItems();
Akron5240b8c2016-05-20 09:17:41 +0200898
Akrone4961b12017-05-10 21:04:46 +0200899 // Use list
900 var shown = 0;
901 var i;
Akron5240b8c2016-05-20 09:17:41 +0200902
Akrone4961b12017-05-10 21:04:46 +0200903 for (i in this._list) {
Akrona92fd8d2016-05-24 21:13:41 +0200904
Akrone4961b12017-05-10 21:04:46 +0200905 // Don't show - it's before offset
906 shown++;
907 if (shown <= off)
908 continue;
Akrona92fd8d2016-05-24 21:13:41 +0200909
Akrone4961b12017-05-10 21:04:46 +0200910 var itemNr = this._list[i];
911 var item = this.item(itemNr);
912 this._append(itemNr);
913
914 if (shown >= (this.limit() + off))
915 break;
916 };
Akron5240b8c2016-05-20 09:17:41 +0200917 };
918
919 // set the slider to the new offset
Akron9c4d1ae2016-05-25 21:43:22 +0200920 this._slider.offset(this.offset);
Akron5240b8c2016-05-20 09:17:41 +0200921 },
922
923
924 // Append item to the shown list based on index
925 _append : function (i) {
926 var item = this.item(i);
927
928 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200929 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200930 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200931 };
932
Akron5240b8c2016-05-20 09:17:41 +0200933
934 // Append element
935 this.element().appendChild(item.element());
936 },
937
938
939 // Prepend item to the shown list based on index
940 _prepend : function (i) {
941 var item = this.item(i);
942
943 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200944 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200945 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200946 };
Akron5240b8c2016-05-20 09:17:41 +0200947
948 var e = this.element();
Akron9c4d1ae2016-05-25 21:43:22 +0200949
Akron5240b8c2016-05-20 09:17:41 +0200950 // Append element after lengthField/prefix/slider
951 e.insertBefore(
Akrone4961b12017-05-10 21:04:46 +0200952 item.element(),
953 e.children[3]
Akron5240b8c2016-05-20 09:17:41 +0200954 );
Nils Diewald2fe12e12015-03-06 16:47:06 +0000955 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000956 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000957});