blob: 7155cdfb32297ee55f25cf880718702853bcecfd [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;
338 // TODO:
339 // Release click event on touchend!
340 }
341 else if (e.type === "touchmove") {
342 var t = e.touches[0];
343 // s.movetoRel(t.clientY - this._initTouch);
344 if ((this._lastTouch + 26) < t.clientY) {
345 this.screen(this.offset - 1);
346 this._lastTouch = t.clientY;
347 }
348 else if ((this._lastTouch - 26) > t.clientY) {
349 this.screen(this.offset + 1);
350 this._lastTouch = t.clientY;
351 }
352 e.halt();
353 };
354 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000355
Nils Diewald59c02fc2015-03-07 01:29:09 +0000356 // Arrow key and prefix treatment
Nils Diewald47f366b2015-04-15 20:06:35 +0000357 _keydown : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000358 var code = _codeFromEvent(e);
359
Nils Diewald59c02fc2015-03-07 01:29:09 +0000360 switch (code) {
361 case 27: // 'Esc'
Akrone4961b12017-05-10 21:04:46 +0200362 e.halt();
363 this.hide();
364 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000365
Nils Diewald59c02fc2015-03-07 01:29:09 +0000366 case 38: // 'Up'
Akrone4961b12017-05-10 21:04:46 +0200367 e.halt();
368 this.prev();
369 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000370 case 33: // 'Page up'
Akrone4961b12017-05-10 21:04:46 +0200371 e.halt();
372 this.pageUp();
373 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000374 case 40: // 'Down'
Akrone4961b12017-05-10 21:04:46 +0200375 e.halt();
376 this.next();
377 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000378 case 34: // 'Page down'
Akrone4961b12017-05-10 21:04:46 +0200379 e.halt();
380 this.pageDown();
381 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000382 case 39: // 'Right'
Akrone4961b12017-05-10 21:04:46 +0200383 if (this._prefix.active())
384 break;
Nils Diewalde8518f82015-03-18 22:41:49 +0000385
Akrone4961b12017-05-10 21:04:46 +0200386 var item = this.liveItem(this.position);
387
388 if (item["further"] !== undefined) {
389 item["further"].bind(item).apply();
390 };
391
392 e.halt();
393 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000394 case 13: // 'Enter'
395
Akrone4961b12017-05-10 21:04:46 +0200396 // Click on prefix
397 if (this._prefix.active())
398 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000399
Akrone4961b12017-05-10 21:04:46 +0200400 // Click on item
401 else
402 this.liveItem(this.position).onclick(e);
403 e.halt();
404 break;
Nils Diewald59c02fc2015-03-07 01:29:09 +0000405 case 8: // 'Backspace'
Akrone4961b12017-05-10 21:04:46 +0200406 this._prefix.chop();
407 this.show();
408 e.halt();
409 break;
Nils Diewald47f366b2015-04-15 20:06:35 +0000410 };
411 },
Nils Diewald59c02fc2015-03-07 01:29:09 +0000412
Nils Diewald47f366b2015-04-15 20:06:35 +0000413 // Add characters to prefix
414 _keypress : function (e) {
Akron9c2f9382016-05-25 16:36:04 +0200415 if (e.charCode !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200416 e.halt();
417 var c = String.fromCharCode(_codeFromEvent(e));
418
419 // Add prefix
420 this._prefix.add(c);
421 this.show();
Akron9c2f9382016-05-25 16:36:04 +0200422 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000423 },
424
Akron47c086c2016-05-18 21:22:06 +0200425 /**
Akron5240b8c2016-05-20 09:17:41 +0200426 * Show a screen with a given offset
427 * in the viewport.
Akron47c086c2016-05-18 21:22:06 +0200428 */
429 screen : function (nr) {
Akron3c2730f2016-05-24 15:08:29 +0200430 if (nr < 0) {
Akrone4961b12017-05-10 21:04:46 +0200431 nr = 0
Akron3c2730f2016-05-24 15:08:29 +0200432 }
Akron6ac58442016-05-24 16:52:29 +0200433 else if (nr > (this.liveLength() - this.limit())) {
Akrone4961b12017-05-10 21:04:46 +0200434 nr = (this.liveLength() - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200435 };
436
Akron9c4d1ae2016-05-25 21:43:22 +0200437 if (this.offset === nr)
Akrone4961b12017-05-10 21:04:46 +0200438 return;
Akron5a1f5bb2016-05-23 22:00:39 +0200439
Akron47c086c2016-05-18 21:22:06 +0200440 this._showItems(nr);
441 },
442
Nils Diewald2fe12e12015-03-06 16:47:06 +0000443 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000444 * Get the associated dom element.
Nils Diewald2fe12e12015-03-06 16:47:06 +0000445 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000446 element : function () {
447 return this._element;
448 },
449
Nils Diewald2fe12e12015-03-06 16:47:06 +0000450 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000451 * Get the creator class for items
Nils Diewald2fe12e12015-03-06 16:47:06 +0000452 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000453 itemClass : function () {
454 return this._itemClass;
455 },
456
457 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000458 * Get and set the numerical value
459 * for the maximum number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000460 */
461 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000462 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200463 if (this._limit !== limit) {
464 this._limit = limit;
465 this._slider.limit(limit).reInit();
466 };
467 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000468 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000469 return this._limit;
470 },
471
Nils Diewald7148c6f2015-05-04 15:07:53 +0000472
Nils Diewald86dad5b2015-01-28 15:09:07 +0000473 /**
474 * Upgrade this object to another object,
475 * while private data stays intact.
476 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000477 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000478 */
479 upgradeTo : function (props) {
480 for (var prop in props) {
Akrone4961b12017-05-10 21:04:46 +0200481 this[prop] = props[prop];
Nils Diewald86dad5b2015-01-28 15:09:07 +0000482 };
483 return this;
484 },
485
Nils Diewald7148c6f2015-05-04 15:07:53 +0000486
Nils Diewald86dad5b2015-01-28 15:09:07 +0000487 /**
Akron97752a72016-05-25 14:43:07 +0200488 * Filter the list and make it visible.
489 * This is always called once the prefix changes.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000490 *
491 * @param {string} Prefix for filtering the list
492 */
Akron6ed13992016-05-23 18:06:05 +0200493 show : function (active) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000494
Akron5240b8c2016-05-20 09:17:41 +0200495 // show menu based on initial offset
Akron6ac58442016-05-24 16:52:29 +0200496 this._unmark(); // Unmark everything that was marked before
Akrona92fd8d2016-05-24 21:13:41 +0200497 this.removeItems();
Akron6ed13992016-05-23 18:06:05 +0200498
499 // Initialize the list
500 if (!this._initList()) {
Akron6ac58442016-05-24 16:52:29 +0200501
Akrone4961b12017-05-10 21:04:46 +0200502 // The prefix is not active
503 this._prefix.active(true);
Akron6ed13992016-05-23 18:06:05 +0200504
Akrone4961b12017-05-10 21:04:46 +0200505 // finally show the element
506 this._element.classList.add('visible');
507
508 return true;
Akron6ed13992016-05-23 18:06:05 +0200509 };
510
511 var offset = 0;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000512
Akron9c2f9382016-05-25 16:36:04 +0200513 // Set a chosen value to active and move the viewport
Akron6ed13992016-05-23 18:06:05 +0200514 if (arguments.length === 1) {
515
Akrone4961b12017-05-10 21:04:46 +0200516 // Normalize active value
517 if (active < 0) {
518 active = 0;
519 }
520 else if (active >= this.liveLength()) {
521 active = this.liveLength() - 1;
522 };
Akron6ed13992016-05-23 18:06:05 +0200523
Akrone4961b12017-05-10 21:04:46 +0200524 // Item is outside the first viewport
525 if (active >= this._limit) {
526 offset = active;
527 if (offset > (this.liveLength() - this._limit)) {
528 offset = this.liveLength() - this._limit;
529 };
530 };
531
532 this.position = active;
Akron6ed13992016-05-23 18:06:05 +0200533 }
534
Akron9c2f9382016-05-25 16:36:04 +0200535 // Choose the first item
Akron6ed13992016-05-23 18:06:05 +0200536 else if (this._firstActive) {
Akrone4961b12017-05-10 21:04:46 +0200537 this.position = 0;
Akron47c086c2016-05-18 21:22:06 +0200538 }
Akroncb351d62016-05-19 23:10:33 +0200539
Akron9c2f9382016-05-25 16:36:04 +0200540 // Choose no item
Akron47c086c2016-05-18 21:22:06 +0200541 else {
Akrone4961b12017-05-10 21:04:46 +0200542 this.position = -1;
Akron6ed13992016-05-23 18:06:05 +0200543 };
544
Akron9c4d1ae2016-05-25 21:43:22 +0200545 this.offset = offset;
Akron6ed13992016-05-23 18:06:05 +0200546 this._showItems(offset); // Show new item list
547
548 // Make chosen value active
549 if (this.position !== -1) {
Akrone4961b12017-05-10 21:04:46 +0200550 this.liveItem(this.position).active(true);
Akron6ed13992016-05-23 18:06:05 +0200551 };
Akron37513a62015-11-17 01:07:11 +0100552
Akron5240b8c2016-05-20 09:17:41 +0200553 // The prefix is not active
Nils Diewalde8518f82015-03-18 22:41:49 +0000554 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000555
Akron5240b8c2016-05-20 09:17:41 +0200556 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200557 this._element.classList.add('visible');
Nils Diewald2fe12e12015-03-06 16:47:06 +0000558
Nils Diewald86dad5b2015-01-28 15:09:07 +0000559 // Add classes for rolling menus
560 this._boundary(true);
Akron6bb71582016-06-10 20:41:08 +0200561
Nils Diewald59c02fc2015-03-07 01:29:09 +0000562 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000563 },
564
Nils Diewald7148c6f2015-05-04 15:07:53 +0000565
566 /**
567 * Hide the menu and call the onHide callback.
568 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000569 hide : function () {
Akrona92fd8d2016-05-24 21:13:41 +0200570 this.removeItems();
Nils Diewald7148c6f2015-05-04 15:07:53 +0000571 this._prefix.clear();
Nils Diewald5c5a7472015-04-02 22:13:38 +0000572 this.onHide();
Akron6bb71582016-06-10 20:41:08 +0200573 this._element.classList.remove('visible');
574
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000575 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000576 },
577
Nils Diewald7148c6f2015-05-04 15:07:53 +0000578 /**
579 * Function released when the menu hides.
580 * This method is expected to be overridden.
581 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000582 onHide : function () {},
583
Nils Diewald86dad5b2015-01-28 15:09:07 +0000584 /**
585 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000586 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000587 */
Nils Diewald5975d702015-03-09 17:45:42 +0000588 prefix : function (pref) {
589 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200590 this._prefix.value(pref);
591 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000592 };
593 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000594 },
595
Akronc7448732016-04-27 14:06:58 +0200596 /**
597 * Get the lengthField object.
598 */
599 lengthField : function () {
600 return this._lengthField;
601 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000602
Akron5240b8c2016-05-20 09:17:41 +0200603 /**
604 * Get the associated slider object.
605 */
606 slider : function () {
607 return this._slider;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000608 },
609
Akron5240b8c2016-05-20 09:17:41 +0200610
Nils Diewald86dad5b2015-01-28 15:09:07 +0000611 /**
612 * Delete all visible items from the menu element
613 */
Akrona92fd8d2016-05-24 21:13:41 +0200614 removeItems : function () {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000615 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000616
Nils Diewald2fe12e12015-03-06 16:47:06 +0000617 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000618 var children = this._element.childNodes;
Akronc7448732016-04-27 14:06:58 +0200619 // Leave the prefix and lengthField
Akron9905e2a2016-05-10 16:06:44 +0200620 for (var i = children.length - 1; i >= 3; i--) {
Akrone4961b12017-05-10 21:04:46 +0200621 this._element.removeChild(
622 children[i]
623 );
Nils Diewald5975d702015-03-09 17:45:42 +0000624 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000625 },
626
Nils Diewald2fe12e12015-03-06 16:47:06 +0000627 /**
628 * Get a specific item from the complete list
629 *
630 * @param {number} index of the list item
631 */
632 item : function (index) {
633 return this._items[index]
634 },
635
636
Nils Diewald86dad5b2015-01-28 15:09:07 +0000637 /**
638 * Get a specific item from the filtered list
639 *
640 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000641 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000642 */
643 liveItem : function (index) {
644 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200645 if (!this._initList())
646 return;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000647
648 return this._items[this._list[index]];
649 },
650
Nils Diewald86dad5b2015-01-28 15:09:07 +0000651
652 /**
Akron5240b8c2016-05-20 09:17:41 +0200653 * Get a specific item from the viewport list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000654 *
655 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000656 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000657 */
658 shownItem : function (index) {
659 if (index >= this.limit())
Akrone4961b12017-05-10 21:04:46 +0200660 return;
Akron9c4d1ae2016-05-25 21:43:22 +0200661 return this.liveItem(this.offset + index);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000662 },
663
664
Nils Diewald2fe12e12015-03-06 16:47:06 +0000665 /**
Akron9c4d1ae2016-05-25 21:43:22 +0200666 * Get the length of the full item list
Nils Diewald2fe12e12015-03-06 16:47:06 +0000667 */
668 length : function () {
669 return this._items.length;
670 },
671
672
673 /**
Akron5240b8c2016-05-20 09:17:41 +0200674 * Length of the filtered item list.
675 */
676 liveLength : function () {
677 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200678 this._initList();
Akron5240b8c2016-05-20 09:17:41 +0200679 return this._list.length;
680 },
681
682
683 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000684 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000685 */
686 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000687
Akronb38afb22016-05-25 19:30:01 +0200688 // No list
689 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200690 return;
Akronb38afb22016-05-25 19:30:01 +0200691
Akron9c4d1ae2016-05-25 21:43:22 +0200692 // Deactivate old item
693 if (this.position !== -1 && !this._prefix.active()) {
Akrone4961b12017-05-10 21:04:46 +0200694 this.liveItem(this.position).active(false);
Akron9c4d1ae2016-05-25 21:43:22 +0200695 };
Nils Diewalde8518f82015-03-18 22:41:49 +0000696
Akron9c4d1ae2016-05-25 21:43:22 +0200697 // Get new active item
698 this.position++;
699 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000700
Nils Diewald5975d702015-03-09 17:45:42 +0000701 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000702 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000703
Akrone4961b12017-05-10 21:04:46 +0200704 // Activate prefix
705 var prefix = this._prefix;
Nils Diewald5975d702015-03-09 17:45:42 +0000706
Akrone4961b12017-05-10 21:04:46 +0200707 // Prefix is set and not active - choose!
708 if (prefix.isSet() && !prefix.active()) {
709 this.position--;
710 prefix.active(true);
711 return;
712 }
Akron9c4d1ae2016-05-25 21:43:22 +0200713
Akrone4961b12017-05-10 21:04:46 +0200714 // Choose first item
715 else {
716 newItem = this.liveItem(0);
717 // choose first item
718 this.position = 0;
719 this._showItems(0);
720 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000721 }
722
Akron5a1f5bb2016-05-23 22:00:39 +0200723 // The next element is after the viewport - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200724 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200725 this.screen(this.position - this.limit() + 1);
Akron5a1f5bb2016-05-23 22:00:39 +0200726 }
727
728 // The next element is before the viewport - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200729 else if (this.position <= this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200730 this.screen(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000731 };
Nils Diewald5975d702015-03-09 17:45:42 +0000732
733 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000734 newItem.active(true);
735 },
736
Nils Diewalde8518f82015-03-18 22:41:49 +0000737 /*
Nils Diewald86dad5b2015-01-28 15:09:07 +0000738 * Make the previous item in the menu active
739 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000740 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +0000741
Akronb38afb22016-05-25 19:30:01 +0200742 // No list
743 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200744 return;
Akronb38afb22016-05-25 19:30:01 +0200745
Akron9c4d1ae2016-05-25 21:43:22 +0200746 // Deactivate old item
Nils Diewald2d210752015-03-09 19:01:15 +0000747 if (!this._prefix.active()) {
Akron9c4d1ae2016-05-25 21:43:22 +0200748
Akrone4961b12017-05-10 21:04:46 +0200749 // No active element set
750 if (this.position === -1) {
751 this.position = this.liveLength();
752 }
Akron9c4d1ae2016-05-25 21:43:22 +0200753
Akrone4961b12017-05-10 21:04:46 +0200754 // No active element set
755 else {
756 this.liveItem(this.position--).active(false);
757 };
Nils Diewald2d210752015-03-09 19:01:15 +0000758 };
759
Akron9c4d1ae2016-05-25 21:43:22 +0200760 // Get new active item
761 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000762
763 // The previous element is undefined - roll to bottom
764 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000765
Akrone4961b12017-05-10 21:04:46 +0200766 // Activate prefix
767 var prefix = this._prefix;
768 var offset = this.liveLength() - this.limit();
769
770 // Normalize offset
771 offset = offset < 0 ? 0 : offset;
Nils Diewald2d210752015-03-09 19:01:15 +0000772
Akrone4961b12017-05-10 21:04:46 +0200773 // Choose the last item
774 this.position = this.liveLength() - 1;
775
776 // Prefix is set and not active - choose!
777 if (prefix.isSet() && !prefix.active()) {
778 this.position++;
779 prefix.active(true);
780 this.offset = offset;
781 return;
782 }
Nils Diewald2d210752015-03-09 19:01:15 +0000783
Akrone4961b12017-05-10 21:04:46 +0200784 // Choose last item
785 else {
786 newItem = this.liveItem(this.position);
787 this._showItems(offset);
788 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000789 }
790
Akron5a1f5bb2016-05-23 22:00:39 +0200791 // The previous element is before the view - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200792 else if (this.position < this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200793 this.screen(this.position);
Akron5a1f5bb2016-05-23 22:00:39 +0200794 }
795
796 // The previous element is after the view - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200797 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200798 this.screen(this.position - this.limit() + 2);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000799 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000800
Nils Diewald5975d702015-03-09 17:45:42 +0000801 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000802 newItem.active(true);
803 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000804
Akron3c2730f2016-05-24 15:08:29 +0200805 /**
806 * Move the page up by limit!
807 */
808 pageUp : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200809 this.screen(this.offset - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200810 },
811
812
813 /**
814 * Move the page down by limit!
815 */
816 pageDown : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200817 this.screen(this.offset + this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200818 },
819
Nils Diewald86dad5b2015-01-28 15:09:07 +0000820
Akron5240b8c2016-05-20 09:17:41 +0200821 // Unmark all items
822 _unmark : function () {
823 for (var i in this._list) {
Akrone4961b12017-05-10 21:04:46 +0200824 var item = this._items[this._list[i]];
825 item.lowlight();
826 item.active(false);
Akron5240b8c2016-05-20 09:17:41 +0200827 };
828 },
829
Akron5240b8c2016-05-20 09:17:41 +0200830 // Set boundary for viewport
831 _boundary : function (bool) {
Akron55a343b2018-04-06 19:57:36 +0200832 if (this._list.length === 0)
833 return;
Akron5240b8c2016-05-20 09:17:41 +0200834 this.item(this._list[0]).noMore(bool);
835 this.item(this._list[this._list.length - 1]).noMore(bool);
836 },
837
838
839 // Append Items that should be shown
840 _showItems : function (off) {
841
Akrona92fd8d2016-05-24 21:13:41 +0200842 // optimization: scroll down one step
Akron9c4d1ae2016-05-25 21:43:22 +0200843 if (this.offset === (off - 1)) {
Akrone4961b12017-05-10 21:04:46 +0200844 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200845
Akrone4961b12017-05-10 21:04:46 +0200846 // Remove the HTML node from the first item
847 // leave lengthField/prefix/slider
848 this._element.removeChild(this._element.children[3]);
849 var pos = this.offset + this.limit() - 1;
850 this._append(this._list[pos]);
Akrona92fd8d2016-05-24 21:13:41 +0200851 }
Akron5240b8c2016-05-20 09:17:41 +0200852
Akrona92fd8d2016-05-24 21:13:41 +0200853 // optimization: scroll up one step
Akron9c4d1ae2016-05-25 21:43:22 +0200854 else if (this.offset === (off + 1)) {
Akrone4961b12017-05-10 21:04:46 +0200855 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200856
Akrone4961b12017-05-10 21:04:46 +0200857 // Remove the HTML node from the last item
858 this._element.removeChild(this._element.lastChild);
Akron9c4d1ae2016-05-25 21:43:22 +0200859
Akrone4961b12017-05-10 21:04:46 +0200860 this._prepend(this._list[this.offset]);
Akrona92fd8d2016-05-24 21:13:41 +0200861 }
862 else {
Akrone4961b12017-05-10 21:04:46 +0200863 this.offset = off;
Akron5240b8c2016-05-20 09:17:41 +0200864
Akrone4961b12017-05-10 21:04:46 +0200865 // Remove all items
866 this.removeItems();
Akron5240b8c2016-05-20 09:17:41 +0200867
Akrone4961b12017-05-10 21:04:46 +0200868 // Use list
869 var shown = 0;
870 var i;
Akron5240b8c2016-05-20 09:17:41 +0200871
Akrone4961b12017-05-10 21:04:46 +0200872 for (i in this._list) {
Akrona92fd8d2016-05-24 21:13:41 +0200873
Akrone4961b12017-05-10 21:04:46 +0200874 // Don't show - it's before offset
875 shown++;
876 if (shown <= off)
877 continue;
Akrona92fd8d2016-05-24 21:13:41 +0200878
Akrone4961b12017-05-10 21:04:46 +0200879 var itemNr = this._list[i];
880 var item = this.item(itemNr);
881 this._append(itemNr);
882
883 if (shown >= (this.limit() + off))
884 break;
885 };
Akron5240b8c2016-05-20 09:17:41 +0200886 };
887
888 // set the slider to the new offset
Akron9c4d1ae2016-05-25 21:43:22 +0200889 this._slider.offset(this.offset);
Akron5240b8c2016-05-20 09:17:41 +0200890 },
891
892
893 // Append item to the shown list based on index
894 _append : function (i) {
895 var item = this.item(i);
896
897 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200898 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200899 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200900 };
901
Akron5240b8c2016-05-20 09:17:41 +0200902
903 // Append element
904 this.element().appendChild(item.element());
905 },
906
907
908 // Prepend item to the shown list based on index
909 _prepend : function (i) {
910 var item = this.item(i);
911
912 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200913 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200914 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200915 };
Akron5240b8c2016-05-20 09:17:41 +0200916
917 var e = this.element();
Akron9c4d1ae2016-05-25 21:43:22 +0200918
Akron5240b8c2016-05-20 09:17:41 +0200919 // Append element after lengthField/prefix/slider
920 e.insertBefore(
Akrone4961b12017-05-10 21:04:46 +0200921 item.element(),
922 e.children[3]
Akron5240b8c2016-05-20 09:17:41 +0200923 );
Nils Diewald2fe12e12015-03-06 16:47:06 +0000924 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000925 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000926});