blob: 8619d899740c4256831ad94003314cf2de0d964e [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.
Akron02360e42016-06-07 13:41:12 +020014 * TODO: Add a .match() method to items for scrolling and probably for prefixing.
15 * TODO: Add static header (for title, sortation fields, but also for menu points like "fragments" and "history".
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,
Akronc7448732016-04-27 14:06:58 +020024 defaultPrefixClass,
Akron9905e2a2016-05-10 16:06:44 +020025 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)
65 params = {};
66
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) {
71 this._prefix = params["prefixClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020072 }
73 else {
74 this._prefix = defaultPrefixClass.create();
75 };
76 this._prefix._menu = this;
77
78 // Add lengthField object
Akron7524be12016-06-01 17:31:33 +020079 if (params["lengthFieldClass"] !== undefined) {
80 this._lengthField = params["lengthFieldClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020081 }
82 else {
83 this._lengthField = defaultLengthFieldClass.create();
84 };
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) {
Akron9c4d1ae2016-05-25 21:43:22 +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());
99 };
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(
Akron5240b8c2016-05-20 09:17:41 +0200106 'keydown',
Akrona92fd8d2016-05-24 21:13:41 +0200107 this._keydown.bind(this),
Akron5240b8c2016-05-20 09:17:41 +0200108 false
109 );
110
111 // Strings
Akron9c4d1ae2016-05-25 21:43:22 +0200112 el.addEventListener(
Akron5240b8c2016-05-20 09:17:41 +0200113 'keypress',
Akrona92fd8d2016-05-24 21:13:41 +0200114 this._keypress.bind(this),
Akron5240b8c2016-05-20 09:17:41 +0200115 false
116 );
117
118 // Mousewheel
Akron9c4d1ae2016-05-25 21:43:22 +0200119 el.addEventListener(
Akron5240b8c2016-05-20 09:17:41 +0200120 'wheel',
Akrona92fd8d2016-05-24 21:13:41 +0200121 this._mousewheel.bind(this),
Akron5240b8c2016-05-20 09:17:41 +0200122 false
123 );
Akron9c4d1ae2016-05-25 21:43:22 +0200124 this._element = el;
Akron5240b8c2016-05-20 09:17:41 +0200125
Akron5240b8c2016-05-20 09:17:41 +0200126 this._items = new Array();
Akron5240b8c2016-05-20 09:17:41 +0200127
Akron9c4d1ae2016-05-25 21:43:22 +0200128 var i = 0;
Akron5240b8c2016-05-20 09:17:41 +0200129 // Initialize item list based on parameters
Akron7524be12016-06-01 17:31:33 +0200130 for (i in list) {
131 var obj = this._itemClass.create(list[i]);
Akron5240b8c2016-05-20 09:17:41 +0200132
133 // This may become circular
134 obj["_menu"] = this;
Akron7524be12016-06-01 17:31:33 +0200135 this._lengthField.add(list[i]);
Akron5240b8c2016-05-20 09:17:41 +0200136 this._items.push(obj);
137 };
138
Akron9c4d1ae2016-05-25 21:43:22 +0200139 this._limit = menuLimit;
140 this._slider.length(this.liveLength())
141 .limit(this._limit)
142 .reInit();
Akron5240b8c2016-05-20 09:17:41 +0200143
Akron5240b8c2016-05-20 09:17:41 +0200144 this._firstActive = false; // Show the first item active always?
Akron9c4d1ae2016-05-25 21:43:22 +0200145 this.offset = 0;
146 this.position = 0;
Akron5240b8c2016-05-20 09:17:41 +0200147 return this;
148 },
149
150 // Initialize the item list
151 _initList : function () {
152
153 // Create a new list
154 if (this._list === undefined) {
155 this._list = [];
156 }
157 else if (this._list.length !== 0) {
158 this._boundary(false);
159 this._list.length = 0;
160 };
161
162 // Offset is initially zero
Akron9c4d1ae2016-05-25 21:43:22 +0200163 this.offset = 0;
Akron5240b8c2016-05-20 09:17:41 +0200164
165 // There is no prefix set
166 if (this.prefix().length <= 0) {
167
168 // add all items to the list and lowlight
Akron97752a72016-05-25 14:43:07 +0200169 var i = 0;
170 for (; i < this._items.length; i++) {
Akron5240b8c2016-05-20 09:17:41 +0200171 this._list.push(i);
172 this._items[i].lowlight();
173 };
174
Akron9c4d1ae2016-05-25 21:43:22 +0200175 this._slider.length(i).reInit();;
Akron97752a72016-05-25 14:43:07 +0200176
Akron5240b8c2016-05-20 09:17:41 +0200177 return true;
178 };
179
180 /*
181 * There is a prefix set, so filter the list!
182 */
183 var pos;
Akron6ffad5d2016-05-24 17:16:58 +0200184 var prefix = " " + this.prefix().toLowerCase();
Akron5240b8c2016-05-20 09:17:41 +0200185
186 // Iterate over all items and choose preferred matching items
187 // i.e. the matching happens at the word start
188 for (pos = 0; pos < this._items.length; pos++) {
Akron6ffad5d2016-05-24 17:16:58 +0200189 if ((this.item(pos).lcField().indexOf(prefix)) >= 0)
Akron5240b8c2016-05-20 09:17:41 +0200190 this._list.push(pos);
191 };
192
193 // The list is empty - so lower your expectations
194 // Iterate over all items and choose matching items
195 // i.e. the matching happens anywhere in the word
Akron6ffad5d2016-05-24 17:16:58 +0200196 prefix = prefix.substring(1);
Akron5240b8c2016-05-20 09:17:41 +0200197 if (this._list.length == 0) {
198 for (pos = 0; pos < this._items.length; pos++) {
Akron6ffad5d2016-05-24 17:16:58 +0200199 if ((this.item(pos).lcField().indexOf(prefix)) >= 0)
Akron5240b8c2016-05-20 09:17:41 +0200200 this._list.push(pos);
201 };
202 };
203
Akron9c4d1ae2016-05-25 21:43:22 +0200204 this._slider.length(this._list.length).reInit();
Akron6ed13992016-05-23 18:06:05 +0200205
Akron5240b8c2016-05-20 09:17:41 +0200206 // Filter was successful - yeah!
207 return this._list.length > 0 ? true : false;
208 },
209
210
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000211 /**
212 * Destroy this menu
213 * (in case you don't trust the
214 * mark and sweep GC)!
215 */
216 destroy : function () {
Akron47c086c2016-05-18 21:22:06 +0200217
Akron5240b8c2016-05-20 09:17:41 +0200218 // Remove circular reference to "this" in menu
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000219 if (this._element != undefined)
220 delete this._element["menu"];
221
Akron5240b8c2016-05-20 09:17:41 +0200222 // Remove circular reference to "this" in items
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000223 for (var i = 0; i < this._items.length; i++) {
224 delete this._items[i]["_menu"];
225 };
Akron5240b8c2016-05-20 09:17:41 +0200226
227 // Remove circular reference to "this" in prefix
Nils Diewald5c5a7472015-04-02 22:13:38 +0000228 delete this._prefix['_menu'];
Akron5240b8c2016-05-20 09:17:41 +0200229 delete this._lengthField['_menu'];
230 delete this._slider['_menu'];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000231 },
232
Nils Diewald7148c6f2015-05-04 15:07:53 +0000233
234 /**
235 * Focus on this menu.
236 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000237 focus : function () {
238 this._element.focus();
239 },
240
Nils Diewald7148c6f2015-05-04 15:07:53 +0000241
Nils Diewald59c02fc2015-03-07 01:29:09 +0000242 // mouse wheel treatment
243 _mousewheel : function (e) {
244 var delta = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000245
246 delta = e.deltaY / 120;
247 if (delta > 0)
Nils Diewald59c02fc2015-03-07 01:29:09 +0000248 this.next();
Nils Diewald5975d702015-03-09 17:45:42 +0000249 else if (delta < 0)
Nils Diewald59c02fc2015-03-07 01:29:09 +0000250 this.prev();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000251 e.halt();
252 },
253
Nils Diewald7148c6f2015-05-04 15:07:53 +0000254
Nils Diewald59c02fc2015-03-07 01:29:09 +0000255 // Arrow key and prefix treatment
Nils Diewald47f366b2015-04-15 20:06:35 +0000256 _keydown : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000257 var code = _codeFromEvent(e);
258
Nils Diewald59c02fc2015-03-07 01:29:09 +0000259 switch (code) {
260 case 27: // 'Esc'
261 e.halt();
262 this.hide();
263 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000264
Nils Diewald59c02fc2015-03-07 01:29:09 +0000265 case 38: // 'Up'
266 e.halt();
267 this.prev();
268 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000269 case 33: // 'Page up'
Nils Diewald59c02fc2015-03-07 01:29:09 +0000270 e.halt();
Akron3c2730f2016-05-24 15:08:29 +0200271 this.pageUp();
Nils Diewald5975d702015-03-09 17:45:42 +0000272 break;
273 case 40: // 'Down'
274 e.halt();
275 this.next();
276 break;
277 case 34: // 'Page down'
278 e.halt();
Akron3c2730f2016-05-24 15:08:29 +0200279 this.pageDown();
Nils Diewald5975d702015-03-09 17:45:42 +0000280 break;
281 case 39: // 'Right'
Nils Diewalde8518f82015-03-18 22:41:49 +0000282 if (this._prefix.active())
283 break;
284
Akronf86eaea2016-05-13 18:02:27 +0200285 var item = this.liveItem(this.position);
Akron5ef4fa02015-06-02 16:25:14 +0200286
Nils Diewald5975d702015-03-09 17:45:42 +0000287 if (item["further"] !== undefined) {
288 item["further"].bind(item).apply();
Nils Diewald5975d702015-03-09 17:45:42 +0000289 };
Akron5ef4fa02015-06-02 16:25:14 +0200290
291 e.halt();
Nils Diewald5975d702015-03-09 17:45:42 +0000292 break;
293 case 13: // 'Enter'
294
295 // Click on prefix
296 if (this._prefix.active())
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000297 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000298
299 // Click on item
300 else
Akronf86eaea2016-05-13 18:02:27 +0200301 this.liveItem(this.position).onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000302 e.halt();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000303 break;
304 case 8: // 'Backspace'
Nils Diewald7148c6f2015-05-04 15:07:53 +0000305 this._prefix.chop();
Nils Diewald5975d702015-03-09 17:45:42 +0000306 this.show();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000307 e.halt();
308 break;
Nils Diewald47f366b2015-04-15 20:06:35 +0000309 };
310 },
Nils Diewald59c02fc2015-03-07 01:29:09 +0000311
Nils Diewald47f366b2015-04-15 20:06:35 +0000312 // Add characters to prefix
313 _keypress : function (e) {
Akron9c2f9382016-05-25 16:36:04 +0200314 if (e.charCode !== 0) {
315 e.halt();
316 var c = String.fromCharCode(_codeFromEvent(e));
Nils Diewald5975d702015-03-09 17:45:42 +0000317
Akron9c2f9382016-05-25 16:36:04 +0200318 // Add prefix
319 this._prefix.add(c);
320 this.show();
321 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000322 },
323
Akron47c086c2016-05-18 21:22:06 +0200324 /**
Akron5240b8c2016-05-20 09:17:41 +0200325 * Show a screen with a given offset
326 * in the viewport.
Akron47c086c2016-05-18 21:22:06 +0200327 */
328 screen : function (nr) {
Akron3c2730f2016-05-24 15:08:29 +0200329 if (nr < 0) {
330 nr = 0
331 }
Akron6ac58442016-05-24 16:52:29 +0200332 else if (nr > (this.liveLength() - this.limit())) {
333 nr = (this.liveLength() - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200334 };
335
Akron9c4d1ae2016-05-25 21:43:22 +0200336 if (this.offset === nr)
Akron47c086c2016-05-18 21:22:06 +0200337 return;
Akron5a1f5bb2016-05-23 22:00:39 +0200338
Akron47c086c2016-05-18 21:22:06 +0200339 this._showItems(nr);
340 },
341
Nils Diewald2fe12e12015-03-06 16:47:06 +0000342 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000343 * Get the associated dom element.
Nils Diewald2fe12e12015-03-06 16:47:06 +0000344 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000345 element : function () {
346 return this._element;
347 },
348
Nils Diewald2fe12e12015-03-06 16:47:06 +0000349 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000350 * Get the creator class for items
Nils Diewald2fe12e12015-03-06 16:47:06 +0000351 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000352 itemClass : function () {
353 return this._itemClass;
354 },
355
356 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000357 * Get and set the numerical value
358 * for the maximum number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000359 */
360 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000361 if (arguments.length === 1) {
Akron5240b8c2016-05-20 09:17:41 +0200362 if (this._limit !== limit) {
363 this._limit = limit;
Akron9c4d1ae2016-05-25 21:43:22 +0200364 this._slider.limit(limit).reInit();
Akron5240b8c2016-05-20 09:17:41 +0200365 };
Nils Diewald5975d702015-03-09 17:45:42 +0000366 return this;
367 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000368 return this._limit;
369 },
370
Nils Diewald7148c6f2015-05-04 15:07:53 +0000371
Nils Diewald86dad5b2015-01-28 15:09:07 +0000372 /**
373 * Upgrade this object to another object,
374 * while private data stays intact.
375 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000376 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000377 */
378 upgradeTo : function (props) {
379 for (var prop in props) {
380 this[prop] = props[prop];
381 };
382 return this;
383 },
384
Nils Diewald7148c6f2015-05-04 15:07:53 +0000385
Nils Diewald86dad5b2015-01-28 15:09:07 +0000386 /**
Akron97752a72016-05-25 14:43:07 +0200387 * Filter the list and make it visible.
388 * This is always called once the prefix changes.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000389 *
390 * @param {string} Prefix for filtering the list
391 */
Akron6ed13992016-05-23 18:06:05 +0200392 show : function (active) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000393
Akron5240b8c2016-05-20 09:17:41 +0200394 // show menu based on initial offset
Akron6ac58442016-05-24 16:52:29 +0200395 this._unmark(); // Unmark everything that was marked before
Akrona92fd8d2016-05-24 21:13:41 +0200396 this.removeItems();
Akron6ed13992016-05-23 18:06:05 +0200397
398 // Initialize the list
399 if (!this._initList()) {
Akron6ac58442016-05-24 16:52:29 +0200400
Akron6ed13992016-05-23 18:06:05 +0200401 // The prefix is not active
402 this._prefix.active(true);
403
404 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200405 this._element.classList.add('visible');
Akron6ed13992016-05-23 18:06:05 +0200406
407 return true;
408 };
409
410 var offset = 0;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000411
Akron9c2f9382016-05-25 16:36:04 +0200412 // Set a chosen value to active and move the viewport
Akron6ed13992016-05-23 18:06:05 +0200413 if (arguments.length === 1) {
414
415 // Normalize active value
Akron3c2730f2016-05-24 15:08:29 +0200416 if (active < 0) {
Akron6ed13992016-05-23 18:06:05 +0200417 active = 0;
Akron3c2730f2016-05-24 15:08:29 +0200418 }
Akron7524be12016-06-01 17:31:33 +0200419 else if (active >= this.liveLength()) {
Akron6ac58442016-05-24 16:52:29 +0200420 active = this.liveLength() - 1;
Akron3c2730f2016-05-24 15:08:29 +0200421 };
Akron6ed13992016-05-23 18:06:05 +0200422
Akron0b92f692016-05-25 22:37:13 +0200423 // Item is outside the first viewport
424 if (active >= this._limit) {
Akron6ed13992016-05-23 18:06:05 +0200425 offset = active;
Akron6ac58442016-05-24 16:52:29 +0200426 if (offset > (this.liveLength() - this._limit)) {
427 offset = this.liveLength() - this._limit;
Akron6ed13992016-05-23 18:06:05 +0200428 };
429 };
430
431 this.position = active;
Akron6ed13992016-05-23 18:06:05 +0200432 }
433
Akron9c2f9382016-05-25 16:36:04 +0200434 // Choose the first item
Akron6ed13992016-05-23 18:06:05 +0200435 else if (this._firstActive) {
Akron47c086c2016-05-18 21:22:06 +0200436 this.position = 0;
Akron47c086c2016-05-18 21:22:06 +0200437 }
Akroncb351d62016-05-19 23:10:33 +0200438
Akron9c2f9382016-05-25 16:36:04 +0200439 // Choose no item
Akron47c086c2016-05-18 21:22:06 +0200440 else {
441 this.position = -1;
Akron6ed13992016-05-23 18:06:05 +0200442 };
443
Akron9c4d1ae2016-05-25 21:43:22 +0200444 this.offset = offset;
Akron6ed13992016-05-23 18:06:05 +0200445 this._showItems(offset); // Show new item list
446
447 // Make chosen value active
448 if (this.position !== -1) {
Akron3c2730f2016-05-24 15:08:29 +0200449 this.liveItem(this.position).active(true);
Akron6ed13992016-05-23 18:06:05 +0200450 };
Akron37513a62015-11-17 01:07:11 +0100451
Akron5240b8c2016-05-20 09:17:41 +0200452 // The prefix is not active
Nils Diewalde8518f82015-03-18 22:41:49 +0000453 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000454
Akron5240b8c2016-05-20 09:17:41 +0200455 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200456 this._element.classList.add('visible');
Nils Diewald2fe12e12015-03-06 16:47:06 +0000457
Nils Diewald86dad5b2015-01-28 15:09:07 +0000458 // Add classes for rolling menus
459 this._boundary(true);
Akron6bb71582016-06-10 20:41:08 +0200460
Nils Diewald59c02fc2015-03-07 01:29:09 +0000461 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000462 },
463
Nils Diewald7148c6f2015-05-04 15:07:53 +0000464
465 /**
466 * Hide the menu and call the onHide callback.
467 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000468 hide : function () {
Akrona92fd8d2016-05-24 21:13:41 +0200469 this.removeItems();
Nils Diewald7148c6f2015-05-04 15:07:53 +0000470 this._prefix.clear();
Nils Diewald5c5a7472015-04-02 22:13:38 +0000471 this.onHide();
Akron6bb71582016-06-10 20:41:08 +0200472 this._element.classList.remove('visible');
473
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000474 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000475 },
476
Nils Diewald7148c6f2015-05-04 15:07:53 +0000477 /**
478 * Function released when the menu hides.
479 * This method is expected to be overridden.
480 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000481 onHide : function () {},
482
Nils Diewald7148c6f2015-05-04 15:07:53 +0000483
Nils Diewald86dad5b2015-01-28 15:09:07 +0000484 /**
485 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000486 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000487 */
Nils Diewald5975d702015-03-09 17:45:42 +0000488 prefix : function (pref) {
489 if (arguments.length === 1) {
490 this._prefix.value(pref);
491 return this;
492 };
493 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000494 },
495
Akronc7448732016-04-27 14:06:58 +0200496 /**
497 * Get the lengthField object.
498 */
499 lengthField : function () {
500 return this._lengthField;
501 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000502
Akron5240b8c2016-05-20 09:17:41 +0200503 /**
504 * Get the associated slider object.
505 */
506 slider : function () {
507 return this._slider;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000508 },
509
Akron5240b8c2016-05-20 09:17:41 +0200510
Nils Diewald86dad5b2015-01-28 15:09:07 +0000511 /**
512 * Delete all visible items from the menu element
513 */
Akrona92fd8d2016-05-24 21:13:41 +0200514 removeItems : function () {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000515 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000516
Nils Diewald2fe12e12015-03-06 16:47:06 +0000517 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000518 var children = this._element.childNodes;
Akronc7448732016-04-27 14:06:58 +0200519 // Leave the prefix and lengthField
Akron9905e2a2016-05-10 16:06:44 +0200520 for (var i = children.length - 1; i >= 3; i--) {
Nils Diewald5975d702015-03-09 17:45:42 +0000521 this._element.removeChild(
522 children[i]
523 );
524 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000525 },
526
Nils Diewald2fe12e12015-03-06 16:47:06 +0000527 /**
528 * Get a specific item from the complete list
529 *
530 * @param {number} index of the list item
531 */
532 item : function (index) {
533 return this._items[index]
534 },
535
536
Nils Diewald86dad5b2015-01-28 15:09:07 +0000537 /**
538 * Get a specific item from the filtered list
539 *
540 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000541 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000542 */
543 liveItem : function (index) {
544 if (this._list === undefined)
545 if (!this._initList())
546 return;
547
548 return this._items[this._list[index]];
549 },
550
Nils Diewald86dad5b2015-01-28 15:09:07 +0000551
552 /**
Akron5240b8c2016-05-20 09:17:41 +0200553 * Get a specific item from the viewport list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000554 *
555 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000556 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000557 */
558 shownItem : function (index) {
559 if (index >= this.limit())
560 return;
Akron9c4d1ae2016-05-25 21:43:22 +0200561 return this.liveItem(this.offset + index);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000562 },
563
564
Nils Diewald2fe12e12015-03-06 16:47:06 +0000565 /**
Akron9c4d1ae2016-05-25 21:43:22 +0200566 * Get the length of the full item list
Nils Diewald2fe12e12015-03-06 16:47:06 +0000567 */
568 length : function () {
569 return this._items.length;
570 },
571
572
573 /**
Akron5240b8c2016-05-20 09:17:41 +0200574 * Length of the filtered item list.
575 */
576 liveLength : function () {
577 if (this._list === undefined)
578 this._initList();
579 return this._list.length;
580 },
581
582
583 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000584 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000585 */
586 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000587
Akronb38afb22016-05-25 19:30:01 +0200588 // No list
589 if (this.liveLength() === 0)
590 return;
591
Akron9c4d1ae2016-05-25 21:43:22 +0200592 // Deactivate old item
593 if (this.position !== -1 && !this._prefix.active()) {
594 this.liveItem(this.position).active(false);
595 };
Nils Diewalde8518f82015-03-18 22:41:49 +0000596
Akron9c4d1ae2016-05-25 21:43:22 +0200597 // Get new active item
598 this.position++;
599 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000600
Nils Diewald5975d702015-03-09 17:45:42 +0000601 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000602 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000603
604 // Activate prefix
605 var prefix = this._prefix;
606
Akron9c4d1ae2016-05-25 21:43:22 +0200607 // Prefix is set and not active - choose!
Nils Diewald5975d702015-03-09 17:45:42 +0000608 if (prefix.isSet() && !prefix.active()) {
Akronf86eaea2016-05-13 18:02:27 +0200609 this.position--;
Nils Diewald5975d702015-03-09 17:45:42 +0000610 prefix.active(true);
611 return;
612 }
Akron9c4d1ae2016-05-25 21:43:22 +0200613
614 // Choose first item
Nils Diewald5975d702015-03-09 17:45:42 +0000615 else {
Nils Diewald5975d702015-03-09 17:45:42 +0000616 newItem = this.liveItem(0);
Akron9c4d1ae2016-05-25 21:43:22 +0200617 // choose first item
618 this.position = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000619 this._showItems(0);
620 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000621 }
622
Akron5a1f5bb2016-05-23 22:00:39 +0200623 // The next element is after the viewport - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200624 else if (this.position >= (this.limit() + this.offset)) {
Akron5a1f5bb2016-05-23 22:00:39 +0200625 this.screen(this.position - this.limit() + 1);
626 }
627
628 // The next element is before the viewport - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200629 else if (this.position <= this.offset) {
Akron5a1f5bb2016-05-23 22:00:39 +0200630 this.screen(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000631 };
Nils Diewald5975d702015-03-09 17:45:42 +0000632
633 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000634 newItem.active(true);
635 },
636
Nils Diewalde8518f82015-03-18 22:41:49 +0000637 /*
Nils Diewald86dad5b2015-01-28 15:09:07 +0000638 * Make the previous item in the menu active
639 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000640 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +0000641
Akronb38afb22016-05-25 19:30:01 +0200642 // No list
643 if (this.liveLength() === 0)
644 return;
645
Akron9c4d1ae2016-05-25 21:43:22 +0200646 // Deactivate old item
Nils Diewald2d210752015-03-09 19:01:15 +0000647 if (!this._prefix.active()) {
Akron9c4d1ae2016-05-25 21:43:22 +0200648
649 // No active element set
650 if (this.position === -1) {
651 this.position = this.liveLength();
652 }
653
654 // No active element set
655 else {
656 this.liveItem(this.position--).active(false);
657 };
Nils Diewald2d210752015-03-09 19:01:15 +0000658 };
659
Akron9c4d1ae2016-05-25 21:43:22 +0200660 // Get new active item
661 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000662
663 // The previous element is undefined - roll to bottom
664 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000665
666 // Activate prefix
667 var prefix = this._prefix;
Akrona92fd8d2016-05-24 21:13:41 +0200668 var offset = this.liveLength() - this.limit();
Nils Diewald2d210752015-03-09 19:01:15 +0000669
670 // Normalize offset
Akrona92fd8d2016-05-24 21:13:41 +0200671 offset = offset < 0 ? 0 : offset;
Nils Diewald2d210752015-03-09 19:01:15 +0000672
Akron9c4d1ae2016-05-25 21:43:22 +0200673 // Choose the last item
Akronf86eaea2016-05-13 18:02:27 +0200674 this.position = this.liveLength() - 1;
Nils Diewald5975d702015-03-09 17:45:42 +0000675
Akron9c4d1ae2016-05-25 21:43:22 +0200676 // Prefix is set and not active - choose!
Nils Diewald5975d702015-03-09 17:45:42 +0000677 if (prefix.isSet() && !prefix.active()) {
Akronf86eaea2016-05-13 18:02:27 +0200678 this.position++;
Nils Diewald5975d702015-03-09 17:45:42 +0000679 prefix.active(true);
Akron9c4d1ae2016-05-25 21:43:22 +0200680 this.offset = offset;
Nils Diewald5975d702015-03-09 17:45:42 +0000681 return;
682 }
Akron9c4d1ae2016-05-25 21:43:22 +0200683
684 // Choose last item
Nils Diewald5975d702015-03-09 17:45:42 +0000685 else {
Akronf86eaea2016-05-13 18:02:27 +0200686 newItem = this.liveItem(this.position);
Akrona92fd8d2016-05-24 21:13:41 +0200687 this._showItems(offset);
Nils Diewald5975d702015-03-09 17:45:42 +0000688 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000689 }
690
Akron5a1f5bb2016-05-23 22:00:39 +0200691 // The previous element is before the view - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200692 else if (this.position < this.offset) {
Akron5a1f5bb2016-05-23 22:00:39 +0200693 this.screen(this.position);
694 }
695
696 // The previous element is after the view - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200697 else if (this.position >= (this.limit() + this.offset)) {
Akron5a1f5bb2016-05-23 22:00:39 +0200698 this.screen(this.position - this.limit() + 2);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000699 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000700
Nils Diewald5975d702015-03-09 17:45:42 +0000701 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000702 newItem.active(true);
703 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000704
Akron3c2730f2016-05-24 15:08:29 +0200705 /**
706 * Move the page up by limit!
707 */
708 pageUp : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200709 this.screen(this.offset - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200710 },
711
712
713 /**
714 * Move the page down by limit!
715 */
716 pageDown : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200717 this.screen(this.offset + this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200718 },
719
Nils Diewald86dad5b2015-01-28 15:09:07 +0000720
Akron5240b8c2016-05-20 09:17:41 +0200721 // Unmark all items
722 _unmark : function () {
723 for (var i in this._list) {
724 var item = this._items[this._list[i]];
725 item.lowlight();
726 item.active(false);
727 };
728 },
729
Akron5240b8c2016-05-20 09:17:41 +0200730 // Set boundary for viewport
731 _boundary : function (bool) {
732 this.item(this._list[0]).noMore(bool);
733 this.item(this._list[this._list.length - 1]).noMore(bool);
734 },
735
736
737 // Append Items that should be shown
738 _showItems : function (off) {
739
Akrona92fd8d2016-05-24 21:13:41 +0200740 // optimization: scroll down one step
Akron9c4d1ae2016-05-25 21:43:22 +0200741 if (this.offset === (off - 1)) {
742 this.offset = off;
743
744 // Remove the HTML node from the first item
745 // leave lengthField/prefix/slider
746 this._element.removeChild(this._element.children[3]);
747 var pos = this.offset + this.limit() - 1;
Akrona92fd8d2016-05-24 21:13:41 +0200748 this._append(this._list[pos]);
749 }
Akron5240b8c2016-05-20 09:17:41 +0200750
Akrona92fd8d2016-05-24 21:13:41 +0200751 // optimization: scroll up one step
Akron9c4d1ae2016-05-25 21:43:22 +0200752 else if (this.offset === (off + 1)) {
753 this.offset = off;
754
755 // Remove the HTML node from the last item
756 this._element.removeChild(this._element.lastChild);
757
758 this._prepend(this._list[this.offset]);
Akrona92fd8d2016-05-24 21:13:41 +0200759 }
760 else {
Akron9c4d1ae2016-05-25 21:43:22 +0200761 this.offset = off;
Akron5240b8c2016-05-20 09:17:41 +0200762
Akrona92fd8d2016-05-24 21:13:41 +0200763 // Remove all items
764 this.removeItems();
Akron5240b8c2016-05-20 09:17:41 +0200765
Akrona92fd8d2016-05-24 21:13:41 +0200766 // Use list
767 var shown = 0;
768 var i;
Akron5240b8c2016-05-20 09:17:41 +0200769
Akrona92fd8d2016-05-24 21:13:41 +0200770 for (i in this._list) {
771
772 // Don't show - it's before offset
773 shown++;
774 if (shown <= off)
775 continue;
776
777 var itemNr = this._list[i];
778 var item = this.item(itemNr);
779 this._append(itemNr);
780
781 if (shown >= (this.limit() + off))
782 break;
783 };
Akron5240b8c2016-05-20 09:17:41 +0200784 };
785
786 // set the slider to the new offset
Akron9c4d1ae2016-05-25 21:43:22 +0200787 this._slider.offset(this.offset);
Akron5240b8c2016-05-20 09:17:41 +0200788 },
789
790
791 // Append item to the shown list based on index
792 _append : function (i) {
793 var item = this.item(i);
794
795 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200796 if (this.prefix().length > 0) {
Akron6ffad5d2016-05-24 17:16:58 +0200797 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200798 };
799
Akron5240b8c2016-05-20 09:17:41 +0200800
801 // Append element
802 this.element().appendChild(item.element());
803 },
804
805
806 // Prepend item to the shown list based on index
807 _prepend : function (i) {
808 var item = this.item(i);
809
810 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200811 if (this.prefix().length > 0) {
Akron6ffad5d2016-05-24 17:16:58 +0200812 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200813 };
Akron5240b8c2016-05-20 09:17:41 +0200814
815 var e = this.element();
Akron9c4d1ae2016-05-25 21:43:22 +0200816
Akron5240b8c2016-05-20 09:17:41 +0200817 // Append element after lengthField/prefix/slider
818 e.insertBefore(
819 item.element(),
820 e.children[3]
821 );
Nils Diewald2fe12e12015-03-06 16:47:06 +0000822 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000823 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000824});