blob: 9f91e96a2c47853239be3cf974d02ec97709caac [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/*
Nils Diewald0e6992a2015-04-14 20:13:52 +00007 * TODO: space is not a valid prefix!
Akron3c2730f2016-05-24 15:08:29 +02008 * TODO: Show the slider briefly on move (whenever screen is called).
Akron9c4d1ae2016-05-25 21:43:22 +02009 * TODO: Ignore alt+ and strg+ key strokes.
Akron0b92f692016-05-25 22:37:13 +020010 * TODO: Should scroll to a chosen value after prefixing, if the chosen value is live
Akron201b72a2016-06-03 01:46:19 +020011 * TODO: Add a "title" to a menu that is not scrollable.
12 * TODO: Make the menu responsive by showing less items on smaller screens
13 * or anytime items would be outside the screen.
Nils Diewald2488d052015-04-09 21:46:02 +000014 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000015define([
16 'menu/item',
17 'menu/prefix',
Akronc7448732016-04-27 14:06:58 +020018 'menu/lengthField',
Akron9905e2a2016-05-10 16:06:44 +020019 'menu/slider',
Nils Diewald0e6992a2015-04-14 20:13:52 +000020 'util'
21], function (defaultItemClass,
Akronc7448732016-04-27 14:06:58 +020022 defaultPrefixClass,
Akron9905e2a2016-05-10 16:06:44 +020023 defaultLengthFieldClass,
24 sliderClass) {
Nils Diewaldfda29d92015-01-22 17:28:01 +000025
Nils Diewald0e6992a2015-04-14 20:13:52 +000026 // Default maximum number of menu items
27 var menuLimit = 8;
28
29 function _codeFromEvent (e) {
30 if (e.charCode && (e.keyCode == 0))
31 return e.charCode
32 return e.keyCode;
Nils Diewald59c02fc2015-03-07 01:29:09 +000033 };
34
Nils Diewald86dad5b2015-01-28 15:09:07 +000035
36 /**
37 * List of items for drop down menu (complete).
38 * Only a sublist of the menu is filtered (live).
39 * Only a sublist of the filtered menu is visible (shown).
40 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000041 return {
Nils Diewald86dad5b2015-01-28 15:09:07 +000042 /**
43 * Create new Menu based on the action prefix
44 * and a list of menu items.
45 *
Akron7524be12016-06-01 17:31:33 +020046 *
47 * Accepts an associative array containg the elements
48 * itemClass, prefixClass, lengthFieldClass
49 *
Nils Diewald86dad5b2015-01-28 15:09:07 +000050 * @this {Menu}
51 * @constructor
52 * @param {string} Context prefix
53 * @param {Array.<Array.<string>>} List of menu items
54 */
Akron7524be12016-06-01 17:31:33 +020055 create : function (list, params) {
56 return Object.create(this)._init(list, params);
Nils Diewald86dad5b2015-01-28 15:09:07 +000057 },
58
Akron5240b8c2016-05-20 09:17:41 +020059 // Initialize list
Akron7524be12016-06-01 17:31:33 +020060 _init : function (list, params) {
Akrona92fd8d2016-05-24 21:13:41 +020061
Akron7524be12016-06-01 17:31:33 +020062 if (params === undefined)
63 params = {};
64
65 this._itemClass = params["itemClass"] || defaultItemClass;
Akron5240b8c2016-05-20 09:17:41 +020066
67 // Add prefix object
Akron7524be12016-06-01 17:31:33 +020068 if (params["prefixClass"] !== undefined) {
69 this._prefix = params["prefixClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020070 }
71 else {
72 this._prefix = defaultPrefixClass.create();
73 };
74 this._prefix._menu = this;
75
76 // Add lengthField object
Akron7524be12016-06-01 17:31:33 +020077 if (params["lengthFieldClass"] !== undefined) {
78 this._lengthField = params["lengthFieldClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020079 }
80 else {
81 this._lengthField = defaultLengthFieldClass.create();
82 };
83 this._lengthField._menu = this;
84
85 // Initialize slider
86 this._slider = sliderClass.create(this);
87
88 // Create the element
Akron9c4d1ae2016-05-25 21:43:22 +020089 var el = document.createElement("ul");
90 with (el) {
Akron9c4d1ae2016-05-25 21:43:22 +020091 style.outline = 0;
92 setAttribute('tabindex', 0);
93 classList.add('menu', 'roll');
94 appendChild(this._prefix.element());
95 appendChild(this._lengthField.element());
96 appendChild(this._slider.element());
97 };
Akron5240b8c2016-05-20 09:17:41 +020098
99 // This has to be cleaned up later on
Akron9c4d1ae2016-05-25 21:43:22 +0200100 el["menu"] = this;
Akron5240b8c2016-05-20 09:17:41 +0200101
102 // Arrow keys
Akron9c4d1ae2016-05-25 21:43:22 +0200103 el.addEventListener(
Akron5240b8c2016-05-20 09:17:41 +0200104 'keydown',
Akrona92fd8d2016-05-24 21:13:41 +0200105 this._keydown.bind(this),
Akron5240b8c2016-05-20 09:17:41 +0200106 false
107 );
108
109 // Strings
Akron9c4d1ae2016-05-25 21:43:22 +0200110 el.addEventListener(
Akron5240b8c2016-05-20 09:17:41 +0200111 'keypress',
Akrona92fd8d2016-05-24 21:13:41 +0200112 this._keypress.bind(this),
Akron5240b8c2016-05-20 09:17:41 +0200113 false
114 );
115
116 // Mousewheel
Akron9c4d1ae2016-05-25 21:43:22 +0200117 el.addEventListener(
Akron5240b8c2016-05-20 09:17:41 +0200118 'wheel',
Akrona92fd8d2016-05-24 21:13:41 +0200119 this._mousewheel.bind(this),
Akron5240b8c2016-05-20 09:17:41 +0200120 false
121 );
Akron9c4d1ae2016-05-25 21:43:22 +0200122 this._element = el;
Akron5240b8c2016-05-20 09:17:41 +0200123
Akron5240b8c2016-05-20 09:17:41 +0200124 this._items = new Array();
Akron5240b8c2016-05-20 09:17:41 +0200125
Akron9c4d1ae2016-05-25 21:43:22 +0200126 var i = 0;
Akron5240b8c2016-05-20 09:17:41 +0200127 // Initialize item list based on parameters
Akron7524be12016-06-01 17:31:33 +0200128 for (i in list) {
129 var obj = this._itemClass.create(list[i]);
Akron5240b8c2016-05-20 09:17:41 +0200130
131 // This may become circular
132 obj["_menu"] = this;
Akron7524be12016-06-01 17:31:33 +0200133 this._lengthField.add(list[i]);
Akron5240b8c2016-05-20 09:17:41 +0200134 this._items.push(obj);
135 };
136
Akron9c4d1ae2016-05-25 21:43:22 +0200137 this._limit = menuLimit;
138 this._slider.length(this.liveLength())
139 .limit(this._limit)
140 .reInit();
Akron5240b8c2016-05-20 09:17:41 +0200141
Akron5240b8c2016-05-20 09:17:41 +0200142 this._firstActive = false; // Show the first item active always?
Akron9c4d1ae2016-05-25 21:43:22 +0200143 this.offset = 0;
144 this.position = 0;
Akron5240b8c2016-05-20 09:17:41 +0200145 return this;
146 },
147
148 // Initialize the item list
149 _initList : function () {
150
151 // Create a new list
152 if (this._list === undefined) {
153 this._list = [];
154 }
155 else if (this._list.length !== 0) {
156 this._boundary(false);
157 this._list.length = 0;
158 };
159
160 // Offset is initially zero
Akron9c4d1ae2016-05-25 21:43:22 +0200161 this.offset = 0;
Akron5240b8c2016-05-20 09:17:41 +0200162
163 // There is no prefix set
164 if (this.prefix().length <= 0) {
165
166 // add all items to the list and lowlight
Akron97752a72016-05-25 14:43:07 +0200167 var i = 0;
168 for (; i < this._items.length; i++) {
Akron5240b8c2016-05-20 09:17:41 +0200169 this._list.push(i);
170 this._items[i].lowlight();
171 };
172
Akron9c4d1ae2016-05-25 21:43:22 +0200173 this._slider.length(i).reInit();;
Akron97752a72016-05-25 14:43:07 +0200174
Akron5240b8c2016-05-20 09:17:41 +0200175 return true;
176 };
177
178 /*
179 * There is a prefix set, so filter the list!
180 */
181 var pos;
Akron6ffad5d2016-05-24 17:16:58 +0200182 var prefix = " " + this.prefix().toLowerCase();
Akron5240b8c2016-05-20 09:17:41 +0200183
184 // Iterate over all items and choose preferred matching items
185 // i.e. the matching happens at the word start
186 for (pos = 0; pos < this._items.length; pos++) {
Akron6ffad5d2016-05-24 17:16:58 +0200187 if ((this.item(pos).lcField().indexOf(prefix)) >= 0)
Akron5240b8c2016-05-20 09:17:41 +0200188 this._list.push(pos);
189 };
190
191 // The list is empty - so lower your expectations
192 // Iterate over all items and choose matching items
193 // i.e. the matching happens anywhere in the word
Akron6ffad5d2016-05-24 17:16:58 +0200194 prefix = prefix.substring(1);
Akron5240b8c2016-05-20 09:17:41 +0200195 if (this._list.length == 0) {
196 for (pos = 0; pos < this._items.length; pos++) {
Akron6ffad5d2016-05-24 17:16:58 +0200197 if ((this.item(pos).lcField().indexOf(prefix)) >= 0)
Akron5240b8c2016-05-20 09:17:41 +0200198 this._list.push(pos);
199 };
200 };
201
Akron9c4d1ae2016-05-25 21:43:22 +0200202 this._slider.length(this._list.length).reInit();
Akron6ed13992016-05-23 18:06:05 +0200203
Akron5240b8c2016-05-20 09:17:41 +0200204 // Filter was successful - yeah!
205 return this._list.length > 0 ? true : false;
206 },
207
208
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000209 /**
210 * Destroy this menu
211 * (in case you don't trust the
212 * mark and sweep GC)!
213 */
214 destroy : function () {
Akron47c086c2016-05-18 21:22:06 +0200215
Akron5240b8c2016-05-20 09:17:41 +0200216 // Remove circular reference to "this" in menu
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000217 if (this._element != undefined)
218 delete this._element["menu"];
219
Akron5240b8c2016-05-20 09:17:41 +0200220 // Remove circular reference to "this" in items
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000221 for (var i = 0; i < this._items.length; i++) {
222 delete this._items[i]["_menu"];
223 };
Akron5240b8c2016-05-20 09:17:41 +0200224
225 // Remove circular reference to "this" in prefix
Nils Diewald5c5a7472015-04-02 22:13:38 +0000226 delete this._prefix['_menu'];
Akron5240b8c2016-05-20 09:17:41 +0200227 delete this._lengthField['_menu'];
228 delete this._slider['_menu'];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000229 },
230
Nils Diewald7148c6f2015-05-04 15:07:53 +0000231
232 /**
233 * Focus on this menu.
234 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000235 focus : function () {
236 this._element.focus();
237 },
238
Nils Diewald7148c6f2015-05-04 15:07:53 +0000239
Nils Diewald59c02fc2015-03-07 01:29:09 +0000240 // mouse wheel treatment
241 _mousewheel : function (e) {
242 var delta = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000243
244 delta = e.deltaY / 120;
245 if (delta > 0)
Nils Diewald59c02fc2015-03-07 01:29:09 +0000246 this.next();
Nils Diewald5975d702015-03-09 17:45:42 +0000247 else if (delta < 0)
Nils Diewald59c02fc2015-03-07 01:29:09 +0000248 this.prev();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000249 e.halt();
250 },
251
Nils Diewald7148c6f2015-05-04 15:07:53 +0000252
Nils Diewald59c02fc2015-03-07 01:29:09 +0000253 // Arrow key and prefix treatment
Nils Diewald47f366b2015-04-15 20:06:35 +0000254 _keydown : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000255 var code = _codeFromEvent(e);
256
Nils Diewald59c02fc2015-03-07 01:29:09 +0000257 switch (code) {
258 case 27: // 'Esc'
259 e.halt();
260 this.hide();
261 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000262
Nils Diewald59c02fc2015-03-07 01:29:09 +0000263 case 38: // 'Up'
264 e.halt();
265 this.prev();
266 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000267 case 33: // 'Page up'
Nils Diewald59c02fc2015-03-07 01:29:09 +0000268 e.halt();
Akron3c2730f2016-05-24 15:08:29 +0200269 this.pageUp();
Nils Diewald5975d702015-03-09 17:45:42 +0000270 break;
271 case 40: // 'Down'
272 e.halt();
273 this.next();
274 break;
275 case 34: // 'Page down'
276 e.halt();
Akron3c2730f2016-05-24 15:08:29 +0200277 this.pageDown();
Nils Diewald5975d702015-03-09 17:45:42 +0000278 break;
279 case 39: // 'Right'
Nils Diewalde8518f82015-03-18 22:41:49 +0000280 if (this._prefix.active())
281 break;
282
Akronf86eaea2016-05-13 18:02:27 +0200283 var item = this.liveItem(this.position);
Akron5ef4fa02015-06-02 16:25:14 +0200284
Nils Diewald5975d702015-03-09 17:45:42 +0000285 if (item["further"] !== undefined) {
286 item["further"].bind(item).apply();
Nils Diewald5975d702015-03-09 17:45:42 +0000287 };
Akron5ef4fa02015-06-02 16:25:14 +0200288
289 e.halt();
Nils Diewald5975d702015-03-09 17:45:42 +0000290 break;
291 case 13: // 'Enter'
292
293 // Click on prefix
294 if (this._prefix.active())
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000295 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000296
297 // Click on item
298 else
Akronf86eaea2016-05-13 18:02:27 +0200299 this.liveItem(this.position).onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000300 e.halt();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000301 break;
302 case 8: // 'Backspace'
Nils Diewald7148c6f2015-05-04 15:07:53 +0000303 this._prefix.chop();
Nils Diewald5975d702015-03-09 17:45:42 +0000304 this.show();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000305 e.halt();
306 break;
Nils Diewald47f366b2015-04-15 20:06:35 +0000307 };
308 },
Nils Diewald59c02fc2015-03-07 01:29:09 +0000309
Nils Diewald47f366b2015-04-15 20:06:35 +0000310 // Add characters to prefix
311 _keypress : function (e) {
Akron9c2f9382016-05-25 16:36:04 +0200312 if (e.charCode !== 0) {
313 e.halt();
314 var c = String.fromCharCode(_codeFromEvent(e));
Nils Diewald5975d702015-03-09 17:45:42 +0000315
Akron9c2f9382016-05-25 16:36:04 +0200316 // Add prefix
317 this._prefix.add(c);
318 this.show();
319 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000320 },
321
Akron47c086c2016-05-18 21:22:06 +0200322 /**
Akron5240b8c2016-05-20 09:17:41 +0200323 * Show a screen with a given offset
324 * in the viewport.
Akron47c086c2016-05-18 21:22:06 +0200325 */
326 screen : function (nr) {
Akron3c2730f2016-05-24 15:08:29 +0200327 if (nr < 0) {
328 nr = 0
329 }
Akron6ac58442016-05-24 16:52:29 +0200330 else if (nr > (this.liveLength() - this.limit())) {
331 nr = (this.liveLength() - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200332 };
333
Akron9c4d1ae2016-05-25 21:43:22 +0200334 if (this.offset === nr)
Akron47c086c2016-05-18 21:22:06 +0200335 return;
Akron5a1f5bb2016-05-23 22:00:39 +0200336
Akron47c086c2016-05-18 21:22:06 +0200337 this._showItems(nr);
338 },
339
Nils Diewald2fe12e12015-03-06 16:47:06 +0000340 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000341 * Get the associated dom element.
Nils Diewald2fe12e12015-03-06 16:47:06 +0000342 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000343 element : function () {
344 return this._element;
345 },
346
Nils Diewald2fe12e12015-03-06 16:47:06 +0000347 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000348 * Get the creator class for items
Nils Diewald2fe12e12015-03-06 16:47:06 +0000349 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000350 itemClass : function () {
351 return this._itemClass;
352 },
353
354 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000355 * Get and set the numerical value
356 * for the maximum number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000357 */
358 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000359 if (arguments.length === 1) {
Akron5240b8c2016-05-20 09:17:41 +0200360 if (this._limit !== limit) {
361 this._limit = limit;
Akron9c4d1ae2016-05-25 21:43:22 +0200362 this._slider.limit(limit).reInit();
Akron5240b8c2016-05-20 09:17:41 +0200363 };
Nils Diewald5975d702015-03-09 17:45:42 +0000364 return this;
365 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000366 return this._limit;
367 },
368
Nils Diewald7148c6f2015-05-04 15:07:53 +0000369
Nils Diewald86dad5b2015-01-28 15:09:07 +0000370 /**
371 * Upgrade this object to another object,
372 * while private data stays intact.
373 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000374 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000375 */
376 upgradeTo : function (props) {
377 for (var prop in props) {
378 this[prop] = props[prop];
379 };
380 return this;
381 },
382
Nils Diewald7148c6f2015-05-04 15:07:53 +0000383
Nils Diewald86dad5b2015-01-28 15:09:07 +0000384 /**
Akron97752a72016-05-25 14:43:07 +0200385 * Filter the list and make it visible.
386 * This is always called once the prefix changes.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000387 *
388 * @param {string} Prefix for filtering the list
389 */
Akron6ed13992016-05-23 18:06:05 +0200390 show : function (active) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000391
Akron5240b8c2016-05-20 09:17:41 +0200392 // show menu based on initial offset
Akron6ac58442016-05-24 16:52:29 +0200393 this._unmark(); // Unmark everything that was marked before
Akrona92fd8d2016-05-24 21:13:41 +0200394 this.removeItems();
Akron6ed13992016-05-23 18:06:05 +0200395
396 // Initialize the list
397 if (!this._initList()) {
Akron6ac58442016-05-24 16:52:29 +0200398
Akron6ed13992016-05-23 18:06:05 +0200399 // The prefix is not active
400 this._prefix.active(true);
401
402 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200403 this._element.classList.add('visible');
Akron6ed13992016-05-23 18:06:05 +0200404
405 return true;
406 };
407
408 var offset = 0;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000409
Akron9c2f9382016-05-25 16:36:04 +0200410 // Set a chosen value to active and move the viewport
Akron6ed13992016-05-23 18:06:05 +0200411 if (arguments.length === 1) {
412
413 // Normalize active value
Akron3c2730f2016-05-24 15:08:29 +0200414 if (active < 0) {
Akron6ed13992016-05-23 18:06:05 +0200415 active = 0;
Akron3c2730f2016-05-24 15:08:29 +0200416 }
Akron7524be12016-06-01 17:31:33 +0200417 else if (active >= this.liveLength()) {
Akron6ac58442016-05-24 16:52:29 +0200418 active = this.liveLength() - 1;
Akron3c2730f2016-05-24 15:08:29 +0200419 };
Akron6ed13992016-05-23 18:06:05 +0200420
Akron0b92f692016-05-25 22:37:13 +0200421 // Item is outside the first viewport
422 if (active >= this._limit) {
Akron6ed13992016-05-23 18:06:05 +0200423 offset = active;
Akron6ac58442016-05-24 16:52:29 +0200424 if (offset > (this.liveLength() - this._limit)) {
425 offset = this.liveLength() - this._limit;
Akron6ed13992016-05-23 18:06:05 +0200426 };
427 };
428
429 this.position = active;
Akron6ed13992016-05-23 18:06:05 +0200430 }
431
Akron9c2f9382016-05-25 16:36:04 +0200432 // Choose the first item
Akron6ed13992016-05-23 18:06:05 +0200433 else if (this._firstActive) {
Akron47c086c2016-05-18 21:22:06 +0200434 this.position = 0;
Akron47c086c2016-05-18 21:22:06 +0200435 }
Akroncb351d62016-05-19 23:10:33 +0200436
Akron9c2f9382016-05-25 16:36:04 +0200437 // Choose no item
Akron47c086c2016-05-18 21:22:06 +0200438 else {
439 this.position = -1;
Akron6ed13992016-05-23 18:06:05 +0200440 };
441
Akron9c4d1ae2016-05-25 21:43:22 +0200442 this.offset = offset;
Akron6ed13992016-05-23 18:06:05 +0200443 this._showItems(offset); // Show new item list
444
445 // Make chosen value active
446 if (this.position !== -1) {
Akron3c2730f2016-05-24 15:08:29 +0200447 this.liveItem(this.position).active(true);
Akron6ed13992016-05-23 18:06:05 +0200448 };
Akron37513a62015-11-17 01:07:11 +0100449
Akron5240b8c2016-05-20 09:17:41 +0200450 // The prefix is not active
Nils Diewalde8518f82015-03-18 22:41:49 +0000451 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000452
Akron5240b8c2016-05-20 09:17:41 +0200453 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200454 this._element.classList.add('visible');
Nils Diewald2fe12e12015-03-06 16:47:06 +0000455
Nils Diewald86dad5b2015-01-28 15:09:07 +0000456 // Add classes for rolling menus
457 this._boundary(true);
Akron6bb71582016-06-10 20:41:08 +0200458
Nils Diewald59c02fc2015-03-07 01:29:09 +0000459 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000460 },
461
Nils Diewald7148c6f2015-05-04 15:07:53 +0000462
463 /**
464 * Hide the menu and call the onHide callback.
465 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000466 hide : function () {
Akrona92fd8d2016-05-24 21:13:41 +0200467 this.removeItems();
Nils Diewald7148c6f2015-05-04 15:07:53 +0000468 this._prefix.clear();
Nils Diewald5c5a7472015-04-02 22:13:38 +0000469 this.onHide();
Akron6bb71582016-06-10 20:41:08 +0200470 this._element.classList.remove('visible');
471
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000472 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000473 },
474
Nils Diewald7148c6f2015-05-04 15:07:53 +0000475 /**
476 * Function released when the menu hides.
477 * This method is expected to be overridden.
478 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000479 onHide : function () {},
480
Nils Diewald7148c6f2015-05-04 15:07:53 +0000481
Nils Diewald86dad5b2015-01-28 15:09:07 +0000482 /**
483 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000484 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000485 */
Nils Diewald5975d702015-03-09 17:45:42 +0000486 prefix : function (pref) {
487 if (arguments.length === 1) {
488 this._prefix.value(pref);
489 return this;
490 };
491 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000492 },
493
Akronc7448732016-04-27 14:06:58 +0200494 /**
495 * Get the lengthField object.
496 */
497 lengthField : function () {
498 return this._lengthField;
499 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000500
Akron5240b8c2016-05-20 09:17:41 +0200501 /**
502 * Get the associated slider object.
503 */
504 slider : function () {
505 return this._slider;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000506 },
507
Akron5240b8c2016-05-20 09:17:41 +0200508
Nils Diewald86dad5b2015-01-28 15:09:07 +0000509 /**
510 * Delete all visible items from the menu element
511 */
Akrona92fd8d2016-05-24 21:13:41 +0200512 removeItems : function () {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000513 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000514
Nils Diewald2fe12e12015-03-06 16:47:06 +0000515 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000516 var children = this._element.childNodes;
Akronc7448732016-04-27 14:06:58 +0200517 // Leave the prefix and lengthField
Akron9905e2a2016-05-10 16:06:44 +0200518 for (var i = children.length - 1; i >= 3; i--) {
Nils Diewald5975d702015-03-09 17:45:42 +0000519 this._element.removeChild(
520 children[i]
521 );
522 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000523 },
524
Nils Diewald2fe12e12015-03-06 16:47:06 +0000525 /**
526 * Get a specific item from the complete list
527 *
528 * @param {number} index of the list item
529 */
530 item : function (index) {
531 return this._items[index]
532 },
533
534
Nils Diewald86dad5b2015-01-28 15:09:07 +0000535 /**
536 * Get a specific item from the filtered list
537 *
538 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000539 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000540 */
541 liveItem : function (index) {
542 if (this._list === undefined)
543 if (!this._initList())
544 return;
545
546 return this._items[this._list[index]];
547 },
548
Nils Diewald86dad5b2015-01-28 15:09:07 +0000549
550 /**
Akron5240b8c2016-05-20 09:17:41 +0200551 * Get a specific item from the viewport list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000552 *
553 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000554 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000555 */
556 shownItem : function (index) {
557 if (index >= this.limit())
558 return;
Akron9c4d1ae2016-05-25 21:43:22 +0200559 return this.liveItem(this.offset + index);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000560 },
561
562
Nils Diewald2fe12e12015-03-06 16:47:06 +0000563 /**
Akron9c4d1ae2016-05-25 21:43:22 +0200564 * Get the length of the full item list
Nils Diewald2fe12e12015-03-06 16:47:06 +0000565 */
566 length : function () {
567 return this._items.length;
568 },
569
570
571 /**
Akron5240b8c2016-05-20 09:17:41 +0200572 * Length of the filtered item list.
573 */
574 liveLength : function () {
575 if (this._list === undefined)
576 this._initList();
577 return this._list.length;
578 },
579
580
581 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000582 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000583 */
584 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000585
Akronb38afb22016-05-25 19:30:01 +0200586 // No list
587 if (this.liveLength() === 0)
588 return;
589
Akron9c4d1ae2016-05-25 21:43:22 +0200590 // Deactivate old item
591 if (this.position !== -1 && !this._prefix.active()) {
592 this.liveItem(this.position).active(false);
593 };
Nils Diewalde8518f82015-03-18 22:41:49 +0000594
Akron9c4d1ae2016-05-25 21:43:22 +0200595 // Get new active item
596 this.position++;
597 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000598
Nils Diewald5975d702015-03-09 17:45:42 +0000599 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000600 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000601
602 // Activate prefix
603 var prefix = this._prefix;
604
Akron9c4d1ae2016-05-25 21:43:22 +0200605 // Prefix is set and not active - choose!
Nils Diewald5975d702015-03-09 17:45:42 +0000606 if (prefix.isSet() && !prefix.active()) {
Akronf86eaea2016-05-13 18:02:27 +0200607 this.position--;
Nils Diewald5975d702015-03-09 17:45:42 +0000608 prefix.active(true);
609 return;
610 }
Akron9c4d1ae2016-05-25 21:43:22 +0200611
612 // Choose first item
Nils Diewald5975d702015-03-09 17:45:42 +0000613 else {
Nils Diewald5975d702015-03-09 17:45:42 +0000614 newItem = this.liveItem(0);
Akron9c4d1ae2016-05-25 21:43:22 +0200615 // choose first item
616 this.position = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000617 this._showItems(0);
618 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000619 }
620
Akron5a1f5bb2016-05-23 22:00:39 +0200621 // The next element is after the viewport - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200622 else if (this.position >= (this.limit() + this.offset)) {
Akron5a1f5bb2016-05-23 22:00:39 +0200623 this.screen(this.position - this.limit() + 1);
624 }
625
626 // The next element is before the viewport - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200627 else if (this.position <= this.offset) {
Akron5a1f5bb2016-05-23 22:00:39 +0200628 this.screen(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000629 };
Nils Diewald5975d702015-03-09 17:45:42 +0000630
631 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000632 newItem.active(true);
633 },
634
Nils Diewalde8518f82015-03-18 22:41:49 +0000635 /*
Nils Diewald86dad5b2015-01-28 15:09:07 +0000636 * Make the previous item in the menu active
637 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000638 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +0000639
Akronb38afb22016-05-25 19:30:01 +0200640 // No list
641 if (this.liveLength() === 0)
642 return;
643
Akron9c4d1ae2016-05-25 21:43:22 +0200644 // Deactivate old item
Nils Diewald2d210752015-03-09 19:01:15 +0000645 if (!this._prefix.active()) {
Akron9c4d1ae2016-05-25 21:43:22 +0200646
647 // No active element set
648 if (this.position === -1) {
649 this.position = this.liveLength();
650 }
651
652 // No active element set
653 else {
654 this.liveItem(this.position--).active(false);
655 };
Nils Diewald2d210752015-03-09 19:01:15 +0000656 };
657
Akron9c4d1ae2016-05-25 21:43:22 +0200658 // Get new active item
659 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000660
661 // The previous element is undefined - roll to bottom
662 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000663
664 // Activate prefix
665 var prefix = this._prefix;
Akrona92fd8d2016-05-24 21:13:41 +0200666 var offset = this.liveLength() - this.limit();
Nils Diewald2d210752015-03-09 19:01:15 +0000667
668 // Normalize offset
Akrona92fd8d2016-05-24 21:13:41 +0200669 offset = offset < 0 ? 0 : offset;
Nils Diewald2d210752015-03-09 19:01:15 +0000670
Akron9c4d1ae2016-05-25 21:43:22 +0200671 // Choose the last item
Akronf86eaea2016-05-13 18:02:27 +0200672 this.position = this.liveLength() - 1;
Nils Diewald5975d702015-03-09 17:45:42 +0000673
Akron9c4d1ae2016-05-25 21:43:22 +0200674 // Prefix is set and not active - choose!
Nils Diewald5975d702015-03-09 17:45:42 +0000675 if (prefix.isSet() && !prefix.active()) {
Akronf86eaea2016-05-13 18:02:27 +0200676 this.position++;
Nils Diewald5975d702015-03-09 17:45:42 +0000677 prefix.active(true);
Akron9c4d1ae2016-05-25 21:43:22 +0200678 this.offset = offset;
Nils Diewald5975d702015-03-09 17:45:42 +0000679 return;
680 }
Akron9c4d1ae2016-05-25 21:43:22 +0200681
682 // Choose last item
Nils Diewald5975d702015-03-09 17:45:42 +0000683 else {
Akronf86eaea2016-05-13 18:02:27 +0200684 newItem = this.liveItem(this.position);
Akrona92fd8d2016-05-24 21:13:41 +0200685 this._showItems(offset);
Nils Diewald5975d702015-03-09 17:45:42 +0000686 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000687 }
688
Akron5a1f5bb2016-05-23 22:00:39 +0200689 // The previous element is before the view - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200690 else if (this.position < this.offset) {
Akron5a1f5bb2016-05-23 22:00:39 +0200691 this.screen(this.position);
692 }
693
694 // The previous element is after the view - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200695 else if (this.position >= (this.limit() + this.offset)) {
Akron5a1f5bb2016-05-23 22:00:39 +0200696 this.screen(this.position - this.limit() + 2);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000697 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000698
Nils Diewald5975d702015-03-09 17:45:42 +0000699 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000700 newItem.active(true);
701 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000702
Akron3c2730f2016-05-24 15:08:29 +0200703 /**
704 * Move the page up by limit!
705 */
706 pageUp : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200707 this.screen(this.offset - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200708 },
709
710
711 /**
712 * Move the page down by limit!
713 */
714 pageDown : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200715 this.screen(this.offset + this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200716 },
717
Nils Diewald86dad5b2015-01-28 15:09:07 +0000718
Akron5240b8c2016-05-20 09:17:41 +0200719 // Unmark all items
720 _unmark : function () {
721 for (var i in this._list) {
722 var item = this._items[this._list[i]];
723 item.lowlight();
724 item.active(false);
725 };
726 },
727
Akron5240b8c2016-05-20 09:17:41 +0200728 // Set boundary for viewport
729 _boundary : function (bool) {
730 this.item(this._list[0]).noMore(bool);
731 this.item(this._list[this._list.length - 1]).noMore(bool);
732 },
733
734
735 // Append Items that should be shown
736 _showItems : function (off) {
737
Akrona92fd8d2016-05-24 21:13:41 +0200738 // optimization: scroll down one step
Akron9c4d1ae2016-05-25 21:43:22 +0200739 if (this.offset === (off - 1)) {
740 this.offset = off;
741
742 // Remove the HTML node from the first item
743 // leave lengthField/prefix/slider
744 this._element.removeChild(this._element.children[3]);
745 var pos = this.offset + this.limit() - 1;
Akrona92fd8d2016-05-24 21:13:41 +0200746 this._append(this._list[pos]);
747 }
Akron5240b8c2016-05-20 09:17:41 +0200748
Akrona92fd8d2016-05-24 21:13:41 +0200749 // optimization: scroll up one step
Akron9c4d1ae2016-05-25 21:43:22 +0200750 else if (this.offset === (off + 1)) {
751 this.offset = off;
752
753 // Remove the HTML node from the last item
754 this._element.removeChild(this._element.lastChild);
755
756 this._prepend(this._list[this.offset]);
Akrona92fd8d2016-05-24 21:13:41 +0200757 }
758 else {
Akron9c4d1ae2016-05-25 21:43:22 +0200759 this.offset = off;
Akron5240b8c2016-05-20 09:17:41 +0200760
Akrona92fd8d2016-05-24 21:13:41 +0200761 // Remove all items
762 this.removeItems();
Akron5240b8c2016-05-20 09:17:41 +0200763
Akrona92fd8d2016-05-24 21:13:41 +0200764 // Use list
765 var shown = 0;
766 var i;
Akron5240b8c2016-05-20 09:17:41 +0200767
Akrona92fd8d2016-05-24 21:13:41 +0200768 for (i in this._list) {
769
770 // Don't show - it's before offset
771 shown++;
772 if (shown <= off)
773 continue;
774
775 var itemNr = this._list[i];
776 var item = this.item(itemNr);
777 this._append(itemNr);
778
779 if (shown >= (this.limit() + off))
780 break;
781 };
Akron5240b8c2016-05-20 09:17:41 +0200782 };
783
784 // set the slider to the new offset
Akron9c4d1ae2016-05-25 21:43:22 +0200785 this._slider.offset(this.offset);
Akron5240b8c2016-05-20 09:17:41 +0200786 },
787
788
789 // Append item to the shown list based on index
790 _append : function (i) {
791 var item = this.item(i);
792
793 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200794 if (this.prefix().length > 0) {
Akron6ffad5d2016-05-24 17:16:58 +0200795 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200796 };
797
Akron5240b8c2016-05-20 09:17:41 +0200798
799 // Append element
800 this.element().appendChild(item.element());
801 },
802
803
804 // Prepend item to the shown list based on index
805 _prepend : function (i) {
806 var item = this.item(i);
807
808 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200809 if (this.prefix().length > 0) {
Akron6ffad5d2016-05-24 17:16:58 +0200810 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200811 };
Akron5240b8c2016-05-20 09:17:41 +0200812
813 var e = this.element();
Akron9c4d1ae2016-05-25 21:43:22 +0200814
Akron5240b8c2016-05-20 09:17:41 +0200815 // Append element after lengthField/prefix/slider
816 e.insertBefore(
817 item.element(),
818 e.children[3]
819 );
Nils Diewald2fe12e12015-03-06 16:47:06 +0000820 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000821 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000822});