blob: 1685da5b6692c3e31d7d11eff772ee19d107506f [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 );
Akron9c4d1ae2016-05-25 21:43:22 +0200124 this._element = el;
Akroneaba63e2018-01-26 19:49:30 +0100125
126 this._limit = menuLimit;
Akrone4961b12017-05-10 21:04:46 +0200127
Akron5240b8c2016-05-20 09:17:41 +0200128 this._items = new Array();
Akron5240b8c2016-05-20 09:17:41 +0200129
Akroneaba63e2018-01-26 19:49:30 +0100130 // TODO:
131 // Make this separate from _init
132 this.readItems(list);
133
134 return this;
135 },
136
137 // Read items to add to list
138 readItems : function (list) {
139
140 this._list = undefined;
141
142 // Remove circular reference to "this" in items
143 for (var i = 0; i < this._items.length; i++) {
144 delete this._items[i]["_menu"];
145 delete this._items[i];
146 };
147
148 this._items = new Array();
149 this.removeItems();
150
151
152 // Initialize items
153 this._lengthField.reset();
154
Akron9c4d1ae2016-05-25 21:43:22 +0200155 var i = 0;
Akron5240b8c2016-05-20 09:17:41 +0200156 // Initialize item list based on parameters
Akron7524be12016-06-01 17:31:33 +0200157 for (i in list) {
Akrone4961b12017-05-10 21:04:46 +0200158 var obj = this._itemClass.create(list[i]);
Akron5240b8c2016-05-20 09:17:41 +0200159
Akrone4961b12017-05-10 21:04:46 +0200160 // This may become circular
161 obj["_menu"] = this;
162 this._lengthField.add(list[i]);
163 this._items.push(obj);
Akron5240b8c2016-05-20 09:17:41 +0200164 };
165
Akron9c4d1ae2016-05-25 21:43:22 +0200166 this._slider.length(this.liveLength())
Akrone4961b12017-05-10 21:04:46 +0200167 .limit(this._limit)
168 .reInit();
169
Akroneaba63e2018-01-26 19:49:30 +0100170 this._firstActive = false;
171 // Show the first item active always?
Akron9c4d1ae2016-05-25 21:43:22 +0200172 this.offset = 0;
173 this.position = 0;
Akron5240b8c2016-05-20 09:17:41 +0200174 },
Akroneaba63e2018-01-26 19:49:30 +0100175
Akron5240b8c2016-05-20 09:17:41 +0200176 // Initialize the item list
177 _initList : function () {
178
179 // Create a new list
180 if (this._list === undefined) {
Akrone4961b12017-05-10 21:04:46 +0200181 this._list = [];
Akron5240b8c2016-05-20 09:17:41 +0200182 }
183 else if (this._list.length !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200184 this._boundary(false);
185 this._list.length = 0;
Akron5240b8c2016-05-20 09:17:41 +0200186 };
187
188 // Offset is initially zero
Akron9c4d1ae2016-05-25 21:43:22 +0200189 this.offset = 0;
Akron5240b8c2016-05-20 09:17:41 +0200190
191 // There is no prefix set
192 if (this.prefix().length <= 0) {
193
Akrone4961b12017-05-10 21:04:46 +0200194 // add all items to the list and lowlight
195 var i = 0;
196 for (; i < this._items.length; i++) {
197 this._list.push(i);
198 this._items[i].lowlight();
199 };
Akron5240b8c2016-05-20 09:17:41 +0200200
Akroneaba63e2018-01-26 19:49:30 +0100201 this._slider.length(i).reInit();
Akron97752a72016-05-25 14:43:07 +0200202
Akrone4961b12017-05-10 21:04:46 +0200203 return true;
Akron5240b8c2016-05-20 09:17:41 +0200204 };
205
206 /*
207 * There is a prefix set, so filter the list!
208 */
209 var pos;
Akronacffc652017-12-18 21:21:25 +0100210 var prefixList = this.prefix().toLowerCase().split(" ");
211
212 var items = [];
213 var maxPoints = 1; // minimum 1
Akron5240b8c2016-05-20 09:17:41 +0200214
215 // Iterate over all items and choose preferred matching items
216 // i.e. the matching happens at the word start
217 for (pos = 0; pos < this._items.length; pos++) {
Akronacffc652017-12-18 21:21:25 +0100218
219 var points = 0;
220
221 for (pref = 0; pref < prefixList.length; pref++) {
222 var prefix = " " + prefixList[pref];
223
224 // Check if it matches at the beginning
Akron3b253d32018-07-15 10:16:06 +0200225 // if ((this.item(pos).lcField().indexOf(prefix)) >= 0) {
226 if ((this.item(pos).lcField().includes(prefix))) {
Akronacffc652017-12-18 21:21:25 +0100227 points += 5;
228 }
229
230 // Check if it matches anywhere
Akron3b253d32018-07-15 10:16:06 +0200231 // else if ((this.item(pos).lcField().indexOf(prefix.substring(1))) >= 0) {
232 else if ((this.item(pos).lcField().includes(prefix.substring(1)))) {
Akronacffc652017-12-18 21:21:25 +0100233 points += 1;
234 };
235 };
236
237 if (points > maxPoints) {
238 this._list = [pos];
239 maxPoints = points;
240 }
241 else if (points == maxPoints) {
Akrone4961b12017-05-10 21:04:46 +0200242 this._list.push(pos);
Akronacffc652017-12-18 21:21:25 +0100243 }
Akron5240b8c2016-05-20 09:17:41 +0200244 };
245
246 // The list is empty - so lower your expectations
247 // Iterate over all items and choose matching items
248 // i.e. the matching happens anywhere in the word
Akronacffc652017-12-18 21:21:25 +0100249 /*
Akron6ffad5d2016-05-24 17:16:58 +0200250 prefix = prefix.substring(1);
Akron5240b8c2016-05-20 09:17:41 +0200251 if (this._list.length == 0) {
Akrone4961b12017-05-10 21:04:46 +0200252 for (pos = 0; pos < this._items.length; pos++) {
253 if ((this.item(pos).lcField().indexOf(prefix)) >= 0)
254 this._list.push(pos);
255 };
Akron5240b8c2016-05-20 09:17:41 +0200256 };
Akronacffc652017-12-18 21:21:25 +0100257 */
Akron5240b8c2016-05-20 09:17:41 +0200258
Akron9c4d1ae2016-05-25 21:43:22 +0200259 this._slider.length(this._list.length).reInit();
Akron6ed13992016-05-23 18:06:05 +0200260
Akron5240b8c2016-05-20 09:17:41 +0200261 // Filter was successful - yeah!
262 return this._list.length > 0 ? true : false;
263 },
264
265
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000266 /**
267 * Destroy this menu
268 * (in case you don't trust the
269 * mark and sweep GC)!
270 */
271 destroy : function () {
Akron47c086c2016-05-18 21:22:06 +0200272
Akron5240b8c2016-05-20 09:17:41 +0200273 // Remove circular reference to "this" in menu
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000274 if (this._element != undefined)
Akrone4961b12017-05-10 21:04:46 +0200275 delete this._element["menu"];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000276
Akron5240b8c2016-05-20 09:17:41 +0200277 // Remove circular reference to "this" in items
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000278 for (var i = 0; i < this._items.length; i++) {
Akrone4961b12017-05-10 21:04:46 +0200279 delete this._items[i]["_menu"];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000280 };
Akron5240b8c2016-05-20 09:17:41 +0200281
282 // Remove circular reference to "this" in prefix
Nils Diewald5c5a7472015-04-02 22:13:38 +0000283 delete this._prefix['_menu'];
Akron5240b8c2016-05-20 09:17:41 +0200284 delete this._lengthField['_menu'];
285 delete this._slider['_menu'];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000286 },
287
Nils Diewald7148c6f2015-05-04 15:07:53 +0000288
289 /**
290 * Focus on this menu.
291 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000292 focus : function () {
293 this._element.focus();
294 },
295
Nils Diewald7148c6f2015-05-04 15:07:53 +0000296
Nils Diewald59c02fc2015-03-07 01:29:09 +0000297 // mouse wheel treatment
298 _mousewheel : function (e) {
299 var delta = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000300
301 delta = e.deltaY / 120;
302 if (delta > 0)
Akrone4961b12017-05-10 21:04:46 +0200303 this.next();
Nils Diewald5975d702015-03-09 17:45:42 +0000304 else if (delta < 0)
Akrone4961b12017-05-10 21:04:46 +0200305 this.prev();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000306 e.halt();
307 },
308
Nils Diewald7148c6f2015-05-04 15:07:53 +0000309
Nils Diewald59c02fc2015-03-07 01:29:09 +0000310 // Arrow key and prefix treatment
Nils Diewald47f366b2015-04-15 20:06:35 +0000311 _keydown : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000312 var code = _codeFromEvent(e);
313
Nils Diewald59c02fc2015-03-07 01:29:09 +0000314 switch (code) {
315 case 27: // 'Esc'
Akrone4961b12017-05-10 21:04:46 +0200316 e.halt();
317 this.hide();
318 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000319
Nils Diewald59c02fc2015-03-07 01:29:09 +0000320 case 38: // 'Up'
Akrone4961b12017-05-10 21:04:46 +0200321 e.halt();
322 this.prev();
323 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000324 case 33: // 'Page up'
Akrone4961b12017-05-10 21:04:46 +0200325 e.halt();
326 this.pageUp();
327 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000328 case 40: // 'Down'
Akrone4961b12017-05-10 21:04:46 +0200329 e.halt();
330 this.next();
331 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000332 case 34: // 'Page down'
Akrone4961b12017-05-10 21:04:46 +0200333 e.halt();
334 this.pageDown();
335 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000336 case 39: // 'Right'
Akrone4961b12017-05-10 21:04:46 +0200337 if (this._prefix.active())
338 break;
Nils Diewalde8518f82015-03-18 22:41:49 +0000339
Akrone4961b12017-05-10 21:04:46 +0200340 var item = this.liveItem(this.position);
341
342 if (item["further"] !== undefined) {
343 item["further"].bind(item).apply();
344 };
345
346 e.halt();
347 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000348 case 13: // 'Enter'
349
Akrone4961b12017-05-10 21:04:46 +0200350 // Click on prefix
351 if (this._prefix.active())
352 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000353
Akrone4961b12017-05-10 21:04:46 +0200354 // Click on item
355 else
356 this.liveItem(this.position).onclick(e);
357 e.halt();
358 break;
Nils Diewald59c02fc2015-03-07 01:29:09 +0000359 case 8: // 'Backspace'
Akrone4961b12017-05-10 21:04:46 +0200360 this._prefix.chop();
361 this.show();
362 e.halt();
363 break;
Nils Diewald47f366b2015-04-15 20:06:35 +0000364 };
365 },
Nils Diewald59c02fc2015-03-07 01:29:09 +0000366
Nils Diewald47f366b2015-04-15 20:06:35 +0000367 // Add characters to prefix
368 _keypress : function (e) {
Akron9c2f9382016-05-25 16:36:04 +0200369 if (e.charCode !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200370 e.halt();
371 var c = String.fromCharCode(_codeFromEvent(e));
372
373 // Add prefix
374 this._prefix.add(c);
375 this.show();
Akron9c2f9382016-05-25 16:36:04 +0200376 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000377 },
378
Akron47c086c2016-05-18 21:22:06 +0200379 /**
Akron5240b8c2016-05-20 09:17:41 +0200380 * Show a screen with a given offset
381 * in the viewport.
Akron47c086c2016-05-18 21:22:06 +0200382 */
383 screen : function (nr) {
Akron3c2730f2016-05-24 15:08:29 +0200384 if (nr < 0) {
Akrone4961b12017-05-10 21:04:46 +0200385 nr = 0
Akron3c2730f2016-05-24 15:08:29 +0200386 }
Akron6ac58442016-05-24 16:52:29 +0200387 else if (nr > (this.liveLength() - this.limit())) {
Akrone4961b12017-05-10 21:04:46 +0200388 nr = (this.liveLength() - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200389 };
390
Akron9c4d1ae2016-05-25 21:43:22 +0200391 if (this.offset === nr)
Akrone4961b12017-05-10 21:04:46 +0200392 return;
Akron5a1f5bb2016-05-23 22:00:39 +0200393
Akron47c086c2016-05-18 21:22:06 +0200394 this._showItems(nr);
395 },
396
Nils Diewald2fe12e12015-03-06 16:47:06 +0000397 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000398 * Get the associated dom element.
Nils Diewald2fe12e12015-03-06 16:47:06 +0000399 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000400 element : function () {
401 return this._element;
402 },
403
Nils Diewald2fe12e12015-03-06 16:47:06 +0000404 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000405 * Get the creator class for items
Nils Diewald2fe12e12015-03-06 16:47:06 +0000406 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000407 itemClass : function () {
408 return this._itemClass;
409 },
410
411 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000412 * Get and set the numerical value
413 * for the maximum number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000414 */
415 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000416 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200417 if (this._limit !== limit) {
418 this._limit = limit;
419 this._slider.limit(limit).reInit();
420 };
421 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000422 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000423 return this._limit;
424 },
425
Nils Diewald7148c6f2015-05-04 15:07:53 +0000426
Nils Diewald86dad5b2015-01-28 15:09:07 +0000427 /**
428 * Upgrade this object to another object,
429 * while private data stays intact.
430 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000431 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000432 */
433 upgradeTo : function (props) {
434 for (var prop in props) {
Akrone4961b12017-05-10 21:04:46 +0200435 this[prop] = props[prop];
Nils Diewald86dad5b2015-01-28 15:09:07 +0000436 };
437 return this;
438 },
439
Nils Diewald7148c6f2015-05-04 15:07:53 +0000440
Nils Diewald86dad5b2015-01-28 15:09:07 +0000441 /**
Akron97752a72016-05-25 14:43:07 +0200442 * Filter the list and make it visible.
443 * This is always called once the prefix changes.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000444 *
445 * @param {string} Prefix for filtering the list
446 */
Akron6ed13992016-05-23 18:06:05 +0200447 show : function (active) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000448
Akron5240b8c2016-05-20 09:17:41 +0200449 // show menu based on initial offset
Akron6ac58442016-05-24 16:52:29 +0200450 this._unmark(); // Unmark everything that was marked before
Akrona92fd8d2016-05-24 21:13:41 +0200451 this.removeItems();
Akron6ed13992016-05-23 18:06:05 +0200452
453 // Initialize the list
454 if (!this._initList()) {
Akron6ac58442016-05-24 16:52:29 +0200455
Akrone4961b12017-05-10 21:04:46 +0200456 // The prefix is not active
457 this._prefix.active(true);
Akron6ed13992016-05-23 18:06:05 +0200458
Akrone4961b12017-05-10 21:04:46 +0200459 // finally show the element
460 this._element.classList.add('visible');
461
462 return true;
Akron6ed13992016-05-23 18:06:05 +0200463 };
464
465 var offset = 0;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000466
Akron9c2f9382016-05-25 16:36:04 +0200467 // Set a chosen value to active and move the viewport
Akron6ed13992016-05-23 18:06:05 +0200468 if (arguments.length === 1) {
469
Akrone4961b12017-05-10 21:04:46 +0200470 // Normalize active value
471 if (active < 0) {
472 active = 0;
473 }
474 else if (active >= this.liveLength()) {
475 active = this.liveLength() - 1;
476 };
Akron6ed13992016-05-23 18:06:05 +0200477
Akrone4961b12017-05-10 21:04:46 +0200478 // Item is outside the first viewport
479 if (active >= this._limit) {
480 offset = active;
481 if (offset > (this.liveLength() - this._limit)) {
482 offset = this.liveLength() - this._limit;
483 };
484 };
485
486 this.position = active;
Akron6ed13992016-05-23 18:06:05 +0200487 }
488
Akron9c2f9382016-05-25 16:36:04 +0200489 // Choose the first item
Akron6ed13992016-05-23 18:06:05 +0200490 else if (this._firstActive) {
Akrone4961b12017-05-10 21:04:46 +0200491 this.position = 0;
Akron47c086c2016-05-18 21:22:06 +0200492 }
Akroncb351d62016-05-19 23:10:33 +0200493
Akron9c2f9382016-05-25 16:36:04 +0200494 // Choose no item
Akron47c086c2016-05-18 21:22:06 +0200495 else {
Akrone4961b12017-05-10 21:04:46 +0200496 this.position = -1;
Akron6ed13992016-05-23 18:06:05 +0200497 };
498
Akron9c4d1ae2016-05-25 21:43:22 +0200499 this.offset = offset;
Akron6ed13992016-05-23 18:06:05 +0200500 this._showItems(offset); // Show new item list
501
502 // Make chosen value active
503 if (this.position !== -1) {
Akrone4961b12017-05-10 21:04:46 +0200504 this.liveItem(this.position).active(true);
Akron6ed13992016-05-23 18:06:05 +0200505 };
Akron37513a62015-11-17 01:07:11 +0100506
Akron5240b8c2016-05-20 09:17:41 +0200507 // The prefix is not active
Nils Diewalde8518f82015-03-18 22:41:49 +0000508 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000509
Akron5240b8c2016-05-20 09:17:41 +0200510 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200511 this._element.classList.add('visible');
Nils Diewald2fe12e12015-03-06 16:47:06 +0000512
Nils Diewald86dad5b2015-01-28 15:09:07 +0000513 // Add classes for rolling menus
514 this._boundary(true);
Akron6bb71582016-06-10 20:41:08 +0200515
Nils Diewald59c02fc2015-03-07 01:29:09 +0000516 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000517 },
518
Nils Diewald7148c6f2015-05-04 15:07:53 +0000519
520 /**
521 * Hide the menu and call the onHide callback.
522 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000523 hide : function () {
Akrona92fd8d2016-05-24 21:13:41 +0200524 this.removeItems();
Nils Diewald7148c6f2015-05-04 15:07:53 +0000525 this._prefix.clear();
Nils Diewald5c5a7472015-04-02 22:13:38 +0000526 this.onHide();
Akron6bb71582016-06-10 20:41:08 +0200527 this._element.classList.remove('visible');
528
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000529 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000530 },
531
Nils Diewald7148c6f2015-05-04 15:07:53 +0000532 /**
533 * Function released when the menu hides.
534 * This method is expected to be overridden.
535 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000536 onHide : function () {},
537
Nils Diewald7148c6f2015-05-04 15:07:53 +0000538
Nils Diewald86dad5b2015-01-28 15:09:07 +0000539 /**
540 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000541 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000542 */
Nils Diewald5975d702015-03-09 17:45:42 +0000543 prefix : function (pref) {
544 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200545 this._prefix.value(pref);
546 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000547 };
548 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000549 },
550
Akronc7448732016-04-27 14:06:58 +0200551 /**
552 * Get the lengthField object.
553 */
554 lengthField : function () {
555 return this._lengthField;
556 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000557
Akron5240b8c2016-05-20 09:17:41 +0200558 /**
559 * Get the associated slider object.
560 */
561 slider : function () {
562 return this._slider;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000563 },
564
Akron5240b8c2016-05-20 09:17:41 +0200565
Nils Diewald86dad5b2015-01-28 15:09:07 +0000566 /**
567 * Delete all visible items from the menu element
568 */
Akrona92fd8d2016-05-24 21:13:41 +0200569 removeItems : function () {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000570 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000571
Nils Diewald2fe12e12015-03-06 16:47:06 +0000572 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000573 var children = this._element.childNodes;
Akronc7448732016-04-27 14:06:58 +0200574 // Leave the prefix and lengthField
Akron9905e2a2016-05-10 16:06:44 +0200575 for (var i = children.length - 1; i >= 3; i--) {
Akrone4961b12017-05-10 21:04:46 +0200576 this._element.removeChild(
577 children[i]
578 );
Nils Diewald5975d702015-03-09 17:45:42 +0000579 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000580 },
581
Nils Diewald2fe12e12015-03-06 16:47:06 +0000582 /**
583 * Get a specific item from the complete list
584 *
585 * @param {number} index of the list item
586 */
587 item : function (index) {
588 return this._items[index]
589 },
590
591
Nils Diewald86dad5b2015-01-28 15:09:07 +0000592 /**
593 * Get a specific item from the filtered list
594 *
595 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000596 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000597 */
598 liveItem : function (index) {
599 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200600 if (!this._initList())
601 return;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000602
603 return this._items[this._list[index]];
604 },
605
Nils Diewald86dad5b2015-01-28 15:09:07 +0000606
607 /**
Akron5240b8c2016-05-20 09:17:41 +0200608 * Get a specific item from the viewport list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000609 *
610 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000611 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000612 */
613 shownItem : function (index) {
614 if (index >= this.limit())
Akrone4961b12017-05-10 21:04:46 +0200615 return;
Akron9c4d1ae2016-05-25 21:43:22 +0200616 return this.liveItem(this.offset + index);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000617 },
618
619
Nils Diewald2fe12e12015-03-06 16:47:06 +0000620 /**
Akron9c4d1ae2016-05-25 21:43:22 +0200621 * Get the length of the full item list
Nils Diewald2fe12e12015-03-06 16:47:06 +0000622 */
623 length : function () {
624 return this._items.length;
625 },
626
627
628 /**
Akron5240b8c2016-05-20 09:17:41 +0200629 * Length of the filtered item list.
630 */
631 liveLength : function () {
632 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200633 this._initList();
Akron5240b8c2016-05-20 09:17:41 +0200634 return this._list.length;
635 },
636
637
638 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000639 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000640 */
641 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000642
Akronb38afb22016-05-25 19:30:01 +0200643 // No list
644 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200645 return;
Akronb38afb22016-05-25 19:30:01 +0200646
Akron9c4d1ae2016-05-25 21:43:22 +0200647 // Deactivate old item
648 if (this.position !== -1 && !this._prefix.active()) {
Akrone4961b12017-05-10 21:04:46 +0200649 this.liveItem(this.position).active(false);
Akron9c4d1ae2016-05-25 21:43:22 +0200650 };
Nils Diewalde8518f82015-03-18 22:41:49 +0000651
Akron9c4d1ae2016-05-25 21:43:22 +0200652 // Get new active item
653 this.position++;
654 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000655
Nils Diewald5975d702015-03-09 17:45:42 +0000656 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000657 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000658
Akrone4961b12017-05-10 21:04:46 +0200659 // Activate prefix
660 var prefix = this._prefix;
Nils Diewald5975d702015-03-09 17:45:42 +0000661
Akrone4961b12017-05-10 21:04:46 +0200662 // Prefix is set and not active - choose!
663 if (prefix.isSet() && !prefix.active()) {
664 this.position--;
665 prefix.active(true);
666 return;
667 }
Akron9c4d1ae2016-05-25 21:43:22 +0200668
Akrone4961b12017-05-10 21:04:46 +0200669 // Choose first item
670 else {
671 newItem = this.liveItem(0);
672 // choose first item
673 this.position = 0;
674 this._showItems(0);
675 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000676 }
677
Akron5a1f5bb2016-05-23 22:00:39 +0200678 // The next element is after the viewport - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200679 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200680 this.screen(this.position - this.limit() + 1);
Akron5a1f5bb2016-05-23 22:00:39 +0200681 }
682
683 // The next element is before the viewport - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200684 else if (this.position <= this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200685 this.screen(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000686 };
Nils Diewald5975d702015-03-09 17:45:42 +0000687
688 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000689 newItem.active(true);
690 },
691
Nils Diewalde8518f82015-03-18 22:41:49 +0000692 /*
Nils Diewald86dad5b2015-01-28 15:09:07 +0000693 * Make the previous item in the menu active
694 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000695 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +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
Nils Diewald2d210752015-03-09 19:01:15 +0000702 if (!this._prefix.active()) {
Akron9c4d1ae2016-05-25 21:43:22 +0200703
Akrone4961b12017-05-10 21:04:46 +0200704 // No active element set
705 if (this.position === -1) {
706 this.position = this.liveLength();
707 }
Akron9c4d1ae2016-05-25 21:43:22 +0200708
Akrone4961b12017-05-10 21:04:46 +0200709 // No active element set
710 else {
711 this.liveItem(this.position--).active(false);
712 };
Nils Diewald2d210752015-03-09 19:01:15 +0000713 };
714
Akron9c4d1ae2016-05-25 21:43:22 +0200715 // Get new active item
716 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000717
718 // The previous element is undefined - roll to bottom
719 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000720
Akrone4961b12017-05-10 21:04:46 +0200721 // Activate prefix
722 var prefix = this._prefix;
723 var offset = this.liveLength() - this.limit();
724
725 // Normalize offset
726 offset = offset < 0 ? 0 : offset;
Nils Diewald2d210752015-03-09 19:01:15 +0000727
Akrone4961b12017-05-10 21:04:46 +0200728 // Choose the last item
729 this.position = this.liveLength() - 1;
730
731 // Prefix is set and not active - choose!
732 if (prefix.isSet() && !prefix.active()) {
733 this.position++;
734 prefix.active(true);
735 this.offset = offset;
736 return;
737 }
Nils Diewald2d210752015-03-09 19:01:15 +0000738
Akrone4961b12017-05-10 21:04:46 +0200739 // Choose last item
740 else {
741 newItem = this.liveItem(this.position);
742 this._showItems(offset);
743 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000744 }
745
Akron5a1f5bb2016-05-23 22:00:39 +0200746 // The previous element is before the view - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200747 else if (this.position < this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200748 this.screen(this.position);
Akron5a1f5bb2016-05-23 22:00:39 +0200749 }
750
751 // The previous element is after the view - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200752 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200753 this.screen(this.position - this.limit() + 2);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000754 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000755
Nils Diewald5975d702015-03-09 17:45:42 +0000756 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000757 newItem.active(true);
758 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000759
Akron3c2730f2016-05-24 15:08:29 +0200760 /**
761 * Move the page up by limit!
762 */
763 pageUp : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200764 this.screen(this.offset - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200765 },
766
767
768 /**
769 * Move the page down by limit!
770 */
771 pageDown : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200772 this.screen(this.offset + this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200773 },
774
Nils Diewald86dad5b2015-01-28 15:09:07 +0000775
Akron5240b8c2016-05-20 09:17:41 +0200776 // Unmark all items
777 _unmark : function () {
778 for (var i in this._list) {
Akrone4961b12017-05-10 21:04:46 +0200779 var item = this._items[this._list[i]];
780 item.lowlight();
781 item.active(false);
Akron5240b8c2016-05-20 09:17:41 +0200782 };
783 },
784
Akron5240b8c2016-05-20 09:17:41 +0200785 // Set boundary for viewport
786 _boundary : function (bool) {
Akron55a343b2018-04-06 19:57:36 +0200787 if (this._list.length === 0)
788 return;
Akron5240b8c2016-05-20 09:17:41 +0200789 this.item(this._list[0]).noMore(bool);
790 this.item(this._list[this._list.length - 1]).noMore(bool);
791 },
792
793
794 // Append Items that should be shown
795 _showItems : function (off) {
796
Akrona92fd8d2016-05-24 21:13:41 +0200797 // optimization: scroll down one step
Akron9c4d1ae2016-05-25 21:43:22 +0200798 if (this.offset === (off - 1)) {
Akrone4961b12017-05-10 21:04:46 +0200799 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200800
Akrone4961b12017-05-10 21:04:46 +0200801 // Remove the HTML node from the first item
802 // leave lengthField/prefix/slider
803 this._element.removeChild(this._element.children[3]);
804 var pos = this.offset + this.limit() - 1;
805 this._append(this._list[pos]);
Akrona92fd8d2016-05-24 21:13:41 +0200806 }
Akron5240b8c2016-05-20 09:17:41 +0200807
Akrona92fd8d2016-05-24 21:13:41 +0200808 // optimization: scroll up one step
Akron9c4d1ae2016-05-25 21:43:22 +0200809 else if (this.offset === (off + 1)) {
Akrone4961b12017-05-10 21:04:46 +0200810 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200811
Akrone4961b12017-05-10 21:04:46 +0200812 // Remove the HTML node from the last item
813 this._element.removeChild(this._element.lastChild);
Akron9c4d1ae2016-05-25 21:43:22 +0200814
Akrone4961b12017-05-10 21:04:46 +0200815 this._prepend(this._list[this.offset]);
Akrona92fd8d2016-05-24 21:13:41 +0200816 }
817 else {
Akrone4961b12017-05-10 21:04:46 +0200818 this.offset = off;
Akron5240b8c2016-05-20 09:17:41 +0200819
Akrone4961b12017-05-10 21:04:46 +0200820 // Remove all items
821 this.removeItems();
Akron5240b8c2016-05-20 09:17:41 +0200822
Akrone4961b12017-05-10 21:04:46 +0200823 // Use list
824 var shown = 0;
825 var i;
Akron5240b8c2016-05-20 09:17:41 +0200826
Akrone4961b12017-05-10 21:04:46 +0200827 for (i in this._list) {
Akrona92fd8d2016-05-24 21:13:41 +0200828
Akrone4961b12017-05-10 21:04:46 +0200829 // Don't show - it's before offset
830 shown++;
831 if (shown <= off)
832 continue;
Akrona92fd8d2016-05-24 21:13:41 +0200833
Akrone4961b12017-05-10 21:04:46 +0200834 var itemNr = this._list[i];
835 var item = this.item(itemNr);
836 this._append(itemNr);
837
838 if (shown >= (this.limit() + off))
839 break;
840 };
Akron5240b8c2016-05-20 09:17:41 +0200841 };
842
843 // set the slider to the new offset
Akron9c4d1ae2016-05-25 21:43:22 +0200844 this._slider.offset(this.offset);
Akron5240b8c2016-05-20 09:17:41 +0200845 },
846
847
848 // Append item to the shown list based on index
849 _append : function (i) {
850 var item = this.item(i);
851
852 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200853 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200854 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200855 };
856
Akron5240b8c2016-05-20 09:17:41 +0200857
858 // Append element
859 this.element().appendChild(item.element());
860 },
861
862
863 // Prepend item to the shown list based on index
864 _prepend : function (i) {
865 var item = this.item(i);
866
867 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200868 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200869 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200870 };
Akron5240b8c2016-05-20 09:17:41 +0200871
872 var e = this.element();
Akron9c4d1ae2016-05-25 21:43:22 +0200873
Akron5240b8c2016-05-20 09:17:41 +0200874 // Append element after lengthField/prefix/slider
875 e.insertBefore(
Akrone4961b12017-05-10 21:04:46 +0200876 item.element(),
877 e.children[3]
Akron5240b8c2016-05-20 09:17:41 +0200878 );
Nils Diewald2fe12e12015-03-06 16:47:06 +0000879 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000880 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000881});