blob: 859e10991cc336a81f78698f88c917a3b6f1300c [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".
Nils Diewald2488d052015-04-09 21:46:02 +000015 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000016define([
17 'menu/item',
18 'menu/prefix',
Akronc7448732016-04-27 14:06:58 +020019 'menu/lengthField',
Akron9905e2a2016-05-10 16:06:44 +020020 'menu/slider',
Nils Diewald0e6992a2015-04-14 20:13:52 +000021 'util'
22], function (defaultItemClass,
Akrone4961b12017-05-10 21:04:46 +020023 defaultPrefixClass,
24 defaultLengthFieldClass,
25 sliderClass) {
Nils Diewaldfda29d92015-01-22 17:28:01 +000026
Nils Diewald0e6992a2015-04-14 20:13:52 +000027 // Default maximum number of menu items
28 var menuLimit = 8;
29
30 function _codeFromEvent (e) {
31 if (e.charCode && (e.keyCode == 0))
32 return e.charCode
33 return e.keyCode;
Nils Diewald59c02fc2015-03-07 01:29:09 +000034 };
35
Nils Diewald86dad5b2015-01-28 15:09:07 +000036
37 /**
38 * List of items for drop down menu (complete).
39 * Only a sublist of the menu is filtered (live).
40 * Only a sublist of the filtered menu is visible (shown).
41 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000042 return {
Nils Diewald86dad5b2015-01-28 15:09:07 +000043 /**
44 * Create new Menu based on the action prefix
45 * and a list of menu items.
46 *
Akron7524be12016-06-01 17:31:33 +020047 *
48 * Accepts an associative array containg the elements
49 * itemClass, prefixClass, lengthFieldClass
50 *
Nils Diewald86dad5b2015-01-28 15:09:07 +000051 * @this {Menu}
52 * @constructor
53 * @param {string} Context prefix
54 * @param {Array.<Array.<string>>} List of menu items
55 */
Akron7524be12016-06-01 17:31:33 +020056 create : function (list, params) {
57 return Object.create(this)._init(list, params);
Nils Diewald86dad5b2015-01-28 15:09:07 +000058 },
59
Akron5240b8c2016-05-20 09:17:41 +020060 // Initialize list
Akron7524be12016-06-01 17:31:33 +020061 _init : function (list, params) {
Akrona92fd8d2016-05-24 21:13:41 +020062
Akron7524be12016-06-01 17:31:33 +020063 if (params === undefined)
Akrone4961b12017-05-10 21:04:46 +020064 params = {};
Akron7524be12016-06-01 17:31:33 +020065
66 this._itemClass = params["itemClass"] || defaultItemClass;
Akron5240b8c2016-05-20 09:17:41 +020067
68 // Add prefix object
Akron7524be12016-06-01 17:31:33 +020069 if (params["prefixClass"] !== undefined) {
Akrone4961b12017-05-10 21:04:46 +020070 this._prefix = params["prefixClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020071 }
72 else {
Akrone4961b12017-05-10 21:04:46 +020073 this._prefix = defaultPrefixClass.create();
Akron5240b8c2016-05-20 09:17:41 +020074 };
75 this._prefix._menu = this;
76
77 // Add lengthField object
Akron7524be12016-06-01 17:31:33 +020078 if (params["lengthFieldClass"] !== undefined) {
Akrone4961b12017-05-10 21:04:46 +020079 this._lengthField = params["lengthFieldClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020080 }
81 else {
Akrone4961b12017-05-10 21:04:46 +020082 this._lengthField = defaultLengthFieldClass.create();
Akron5240b8c2016-05-20 09:17:41 +020083 };
84 this._lengthField._menu = this;
85
86 // Initialize slider
87 this._slider = sliderClass.create(this);
88
89 // Create the element
Akron9c4d1ae2016-05-25 21:43:22 +020090 var el = document.createElement("ul");
91 with (el) {
Akrone4961b12017-05-10 21:04:46 +020092 style.outline = 0;
93 setAttribute('tabindex', 0);
94 classList.add('menu', 'roll');
95 appendChild(this._prefix.element());
96 appendChild(this._lengthField.element());
97 appendChild(this._slider.element());
Akron9c4d1ae2016-05-25 21:43:22 +020098 };
Akron5240b8c2016-05-20 09:17:41 +020099
100 // This has to be cleaned up later on
Akron9c4d1ae2016-05-25 21:43:22 +0200101 el["menu"] = this;
Akron5240b8c2016-05-20 09:17:41 +0200102
103 // Arrow keys
Akron9c4d1ae2016-05-25 21:43:22 +0200104 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200105 'keydown',
106 this._keydown.bind(this),
107 false
Akron5240b8c2016-05-20 09:17:41 +0200108 );
109
110 // Strings
Akron9c4d1ae2016-05-25 21:43:22 +0200111 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200112 'keypress',
113 this._keypress.bind(this),
114 false
Akron5240b8c2016-05-20 09:17:41 +0200115 );
116
117 // Mousewheel
Akron9c4d1ae2016-05-25 21:43:22 +0200118 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200119 'wheel',
120 this._mousewheel.bind(this),
121 false
Akron5240b8c2016-05-20 09:17:41 +0200122 );
Akron9c4d1ae2016-05-25 21:43:22 +0200123 this._element = el;
Akrone4961b12017-05-10 21:04:46 +0200124
Akron5240b8c2016-05-20 09:17:41 +0200125 this._items = new Array();
Akron5240b8c2016-05-20 09:17:41 +0200126
Akron9c4d1ae2016-05-25 21:43:22 +0200127 var i = 0;
Akron5240b8c2016-05-20 09:17:41 +0200128 // Initialize item list based on parameters
Akron7524be12016-06-01 17:31:33 +0200129 for (i in list) {
Akrone4961b12017-05-10 21:04:46 +0200130 var obj = this._itemClass.create(list[i]);
Akron5240b8c2016-05-20 09:17:41 +0200131
Akrone4961b12017-05-10 21:04:46 +0200132 // This may become circular
133 obj["_menu"] = this;
134 this._lengthField.add(list[i]);
135 this._items.push(obj);
Akron5240b8c2016-05-20 09:17:41 +0200136 };
137
Akron9c4d1ae2016-05-25 21:43:22 +0200138 this._limit = menuLimit;
139 this._slider.length(this.liveLength())
Akrone4961b12017-05-10 21:04:46 +0200140 .limit(this._limit)
141 .reInit();
142
Akron5240b8c2016-05-20 09:17:41 +0200143 this._firstActive = false; // Show the first item active always?
Akron9c4d1ae2016-05-25 21:43:22 +0200144 this.offset = 0;
145 this.position = 0;
Akron5240b8c2016-05-20 09:17:41 +0200146 return this;
147 },
148
149 // Initialize the item list
150 _initList : function () {
151
152 // Create a new list
153 if (this._list === undefined) {
Akrone4961b12017-05-10 21:04:46 +0200154 this._list = [];
Akron5240b8c2016-05-20 09:17:41 +0200155 }
156 else if (this._list.length !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200157 this._boundary(false);
158 this._list.length = 0;
Akron5240b8c2016-05-20 09:17:41 +0200159 };
160
161 // Offset is initially zero
Akron9c4d1ae2016-05-25 21:43:22 +0200162 this.offset = 0;
Akron5240b8c2016-05-20 09:17:41 +0200163
164 // There is no prefix set
165 if (this.prefix().length <= 0) {
166
Akrone4961b12017-05-10 21:04:46 +0200167 // add all items to the list and lowlight
168 var i = 0;
169 for (; i < this._items.length; i++) {
170 this._list.push(i);
171 this._items[i].lowlight();
172 };
Akron5240b8c2016-05-20 09:17:41 +0200173
Akrone4961b12017-05-10 21:04:46 +0200174 this._slider.length(i).reInit();;
Akron97752a72016-05-25 14:43:07 +0200175
Akrone4961b12017-05-10 21:04:46 +0200176 return true;
Akron5240b8c2016-05-20 09:17:41 +0200177 };
178
179 /*
180 * There is a prefix set, so filter the list!
181 */
182 var pos;
Akronacffc652017-12-18 21:21:25 +0100183 var prefixList = this.prefix().toLowerCase().split(" ");
184
185 var items = [];
186 var maxPoints = 1; // minimum 1
Akron5240b8c2016-05-20 09:17:41 +0200187
188 // Iterate over all items and choose preferred matching items
189 // i.e. the matching happens at the word start
190 for (pos = 0; pos < this._items.length; pos++) {
Akronacffc652017-12-18 21:21:25 +0100191
192 var points = 0;
193
194 for (pref = 0; pref < prefixList.length; pref++) {
195 var prefix = " " + prefixList[pref];
196
197 // Check if it matches at the beginning
198 if ((this.item(pos).lcField().indexOf(prefix)) >= 0) {
199 points += 5;
200 }
201
202 // Check if it matches anywhere
203 else if ((this.item(pos).lcField().indexOf(prefix.substring(1))) >= 0) {
204 points += 1;
205 };
206 };
207
208 if (points > maxPoints) {
209 this._list = [pos];
210 maxPoints = points;
211 }
212 else if (points == maxPoints) {
Akrone4961b12017-05-10 21:04:46 +0200213 this._list.push(pos);
Akronacffc652017-12-18 21:21:25 +0100214 }
Akron5240b8c2016-05-20 09:17:41 +0200215 };
216
217 // The list is empty - so lower your expectations
218 // Iterate over all items and choose matching items
219 // i.e. the matching happens anywhere in the word
Akronacffc652017-12-18 21:21:25 +0100220 /*
Akron6ffad5d2016-05-24 17:16:58 +0200221 prefix = prefix.substring(1);
Akron5240b8c2016-05-20 09:17:41 +0200222 if (this._list.length == 0) {
Akrone4961b12017-05-10 21:04:46 +0200223 for (pos = 0; pos < this._items.length; pos++) {
224 if ((this.item(pos).lcField().indexOf(prefix)) >= 0)
225 this._list.push(pos);
226 };
Akron5240b8c2016-05-20 09:17:41 +0200227 };
Akronacffc652017-12-18 21:21:25 +0100228 */
Akron5240b8c2016-05-20 09:17:41 +0200229
Akron9c4d1ae2016-05-25 21:43:22 +0200230 this._slider.length(this._list.length).reInit();
Akron6ed13992016-05-23 18:06:05 +0200231
Akron5240b8c2016-05-20 09:17:41 +0200232 // Filter was successful - yeah!
233 return this._list.length > 0 ? true : false;
234 },
235
236
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000237 /**
238 * Destroy this menu
239 * (in case you don't trust the
240 * mark and sweep GC)!
241 */
242 destroy : function () {
Akron47c086c2016-05-18 21:22:06 +0200243
Akron5240b8c2016-05-20 09:17:41 +0200244 // Remove circular reference to "this" in menu
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000245 if (this._element != undefined)
Akrone4961b12017-05-10 21:04:46 +0200246 delete this._element["menu"];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000247
Akron5240b8c2016-05-20 09:17:41 +0200248 // Remove circular reference to "this" in items
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000249 for (var i = 0; i < this._items.length; i++) {
Akrone4961b12017-05-10 21:04:46 +0200250 delete this._items[i]["_menu"];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000251 };
Akron5240b8c2016-05-20 09:17:41 +0200252
253 // Remove circular reference to "this" in prefix
Nils Diewald5c5a7472015-04-02 22:13:38 +0000254 delete this._prefix['_menu'];
Akron5240b8c2016-05-20 09:17:41 +0200255 delete this._lengthField['_menu'];
256 delete this._slider['_menu'];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000257 },
258
Nils Diewald7148c6f2015-05-04 15:07:53 +0000259
260 /**
261 * Focus on this menu.
262 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000263 focus : function () {
264 this._element.focus();
265 },
266
Nils Diewald7148c6f2015-05-04 15:07:53 +0000267
Nils Diewald59c02fc2015-03-07 01:29:09 +0000268 // mouse wheel treatment
269 _mousewheel : function (e) {
270 var delta = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000271
272 delta = e.deltaY / 120;
273 if (delta > 0)
Akrone4961b12017-05-10 21:04:46 +0200274 this.next();
Nils Diewald5975d702015-03-09 17:45:42 +0000275 else if (delta < 0)
Akrone4961b12017-05-10 21:04:46 +0200276 this.prev();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000277 e.halt();
278 },
279
Nils Diewald7148c6f2015-05-04 15:07:53 +0000280
Nils Diewald59c02fc2015-03-07 01:29:09 +0000281 // Arrow key and prefix treatment
Nils Diewald47f366b2015-04-15 20:06:35 +0000282 _keydown : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000283 var code = _codeFromEvent(e);
284
Nils Diewald59c02fc2015-03-07 01:29:09 +0000285 switch (code) {
286 case 27: // 'Esc'
Akrone4961b12017-05-10 21:04:46 +0200287 e.halt();
288 this.hide();
289 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000290
Nils Diewald59c02fc2015-03-07 01:29:09 +0000291 case 38: // 'Up'
Akrone4961b12017-05-10 21:04:46 +0200292 e.halt();
293 this.prev();
294 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000295 case 33: // 'Page up'
Akrone4961b12017-05-10 21:04:46 +0200296 e.halt();
297 this.pageUp();
298 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000299 case 40: // 'Down'
Akrone4961b12017-05-10 21:04:46 +0200300 e.halt();
301 this.next();
302 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000303 case 34: // 'Page down'
Akrone4961b12017-05-10 21:04:46 +0200304 e.halt();
305 this.pageDown();
306 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000307 case 39: // 'Right'
Akrone4961b12017-05-10 21:04:46 +0200308 if (this._prefix.active())
309 break;
Nils Diewalde8518f82015-03-18 22:41:49 +0000310
Akrone4961b12017-05-10 21:04:46 +0200311 var item = this.liveItem(this.position);
312
313 if (item["further"] !== undefined) {
314 item["further"].bind(item).apply();
315 };
316
317 e.halt();
318 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000319 case 13: // 'Enter'
320
Akrone4961b12017-05-10 21:04:46 +0200321 // Click on prefix
322 if (this._prefix.active())
323 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000324
Akrone4961b12017-05-10 21:04:46 +0200325 // Click on item
326 else
327 this.liveItem(this.position).onclick(e);
328 e.halt();
329 break;
Nils Diewald59c02fc2015-03-07 01:29:09 +0000330 case 8: // 'Backspace'
Akrone4961b12017-05-10 21:04:46 +0200331 this._prefix.chop();
332 this.show();
333 e.halt();
334 break;
Nils Diewald47f366b2015-04-15 20:06:35 +0000335 };
336 },
Nils Diewald59c02fc2015-03-07 01:29:09 +0000337
Nils Diewald47f366b2015-04-15 20:06:35 +0000338 // Add characters to prefix
339 _keypress : function (e) {
Akron9c2f9382016-05-25 16:36:04 +0200340 if (e.charCode !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200341 e.halt();
342 var c = String.fromCharCode(_codeFromEvent(e));
343
344 // Add prefix
345 this._prefix.add(c);
346 this.show();
Akron9c2f9382016-05-25 16:36:04 +0200347 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000348 },
349
Akron47c086c2016-05-18 21:22:06 +0200350 /**
Akron5240b8c2016-05-20 09:17:41 +0200351 * Show a screen with a given offset
352 * in the viewport.
Akron47c086c2016-05-18 21:22:06 +0200353 */
354 screen : function (nr) {
Akron3c2730f2016-05-24 15:08:29 +0200355 if (nr < 0) {
Akrone4961b12017-05-10 21:04:46 +0200356 nr = 0
Akron3c2730f2016-05-24 15:08:29 +0200357 }
Akron6ac58442016-05-24 16:52:29 +0200358 else if (nr > (this.liveLength() - this.limit())) {
Akrone4961b12017-05-10 21:04:46 +0200359 nr = (this.liveLength() - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200360 };
361
Akron9c4d1ae2016-05-25 21:43:22 +0200362 if (this.offset === nr)
Akrone4961b12017-05-10 21:04:46 +0200363 return;
Akron5a1f5bb2016-05-23 22:00:39 +0200364
Akron47c086c2016-05-18 21:22:06 +0200365 this._showItems(nr);
366 },
367
Nils Diewald2fe12e12015-03-06 16:47:06 +0000368 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000369 * Get the associated dom element.
Nils Diewald2fe12e12015-03-06 16:47:06 +0000370 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000371 element : function () {
372 return this._element;
373 },
374
Nils Diewald2fe12e12015-03-06 16:47:06 +0000375 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000376 * Get the creator class for items
Nils Diewald2fe12e12015-03-06 16:47:06 +0000377 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000378 itemClass : function () {
379 return this._itemClass;
380 },
381
382 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000383 * Get and set the numerical value
384 * for the maximum number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000385 */
386 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000387 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200388 if (this._limit !== limit) {
389 this._limit = limit;
390 this._slider.limit(limit).reInit();
391 };
392 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000393 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000394 return this._limit;
395 },
396
Nils Diewald7148c6f2015-05-04 15:07:53 +0000397
Nils Diewald86dad5b2015-01-28 15:09:07 +0000398 /**
399 * Upgrade this object to another object,
400 * while private data stays intact.
401 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000402 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000403 */
404 upgradeTo : function (props) {
405 for (var prop in props) {
Akrone4961b12017-05-10 21:04:46 +0200406 this[prop] = props[prop];
Nils Diewald86dad5b2015-01-28 15:09:07 +0000407 };
408 return this;
409 },
410
Nils Diewald7148c6f2015-05-04 15:07:53 +0000411
Nils Diewald86dad5b2015-01-28 15:09:07 +0000412 /**
Akron97752a72016-05-25 14:43:07 +0200413 * Filter the list and make it visible.
414 * This is always called once the prefix changes.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000415 *
416 * @param {string} Prefix for filtering the list
417 */
Akron6ed13992016-05-23 18:06:05 +0200418 show : function (active) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000419
Akron5240b8c2016-05-20 09:17:41 +0200420 // show menu based on initial offset
Akron6ac58442016-05-24 16:52:29 +0200421 this._unmark(); // Unmark everything that was marked before
Akrona92fd8d2016-05-24 21:13:41 +0200422 this.removeItems();
Akron6ed13992016-05-23 18:06:05 +0200423
424 // Initialize the list
425 if (!this._initList()) {
Akron6ac58442016-05-24 16:52:29 +0200426
Akrone4961b12017-05-10 21:04:46 +0200427 // The prefix is not active
428 this._prefix.active(true);
Akron6ed13992016-05-23 18:06:05 +0200429
Akrone4961b12017-05-10 21:04:46 +0200430 // finally show the element
431 this._element.classList.add('visible');
432
433 return true;
Akron6ed13992016-05-23 18:06:05 +0200434 };
435
436 var offset = 0;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000437
Akron9c2f9382016-05-25 16:36:04 +0200438 // Set a chosen value to active and move the viewport
Akron6ed13992016-05-23 18:06:05 +0200439 if (arguments.length === 1) {
440
Akrone4961b12017-05-10 21:04:46 +0200441 // Normalize active value
442 if (active < 0) {
443 active = 0;
444 }
445 else if (active >= this.liveLength()) {
446 active = this.liveLength() - 1;
447 };
Akron6ed13992016-05-23 18:06:05 +0200448
Akrone4961b12017-05-10 21:04:46 +0200449 // Item is outside the first viewport
450 if (active >= this._limit) {
451 offset = active;
452 if (offset > (this.liveLength() - this._limit)) {
453 offset = this.liveLength() - this._limit;
454 };
455 };
456
457 this.position = active;
Akron6ed13992016-05-23 18:06:05 +0200458 }
459
Akron9c2f9382016-05-25 16:36:04 +0200460 // Choose the first item
Akron6ed13992016-05-23 18:06:05 +0200461 else if (this._firstActive) {
Akrone4961b12017-05-10 21:04:46 +0200462 this.position = 0;
Akron47c086c2016-05-18 21:22:06 +0200463 }
Akroncb351d62016-05-19 23:10:33 +0200464
Akron9c2f9382016-05-25 16:36:04 +0200465 // Choose no item
Akron47c086c2016-05-18 21:22:06 +0200466 else {
Akrone4961b12017-05-10 21:04:46 +0200467 this.position = -1;
Akron6ed13992016-05-23 18:06:05 +0200468 };
469
Akron9c4d1ae2016-05-25 21:43:22 +0200470 this.offset = offset;
Akron6ed13992016-05-23 18:06:05 +0200471 this._showItems(offset); // Show new item list
472
473 // Make chosen value active
474 if (this.position !== -1) {
Akrone4961b12017-05-10 21:04:46 +0200475 this.liveItem(this.position).active(true);
Akron6ed13992016-05-23 18:06:05 +0200476 };
Akron37513a62015-11-17 01:07:11 +0100477
Akron5240b8c2016-05-20 09:17:41 +0200478 // The prefix is not active
Nils Diewalde8518f82015-03-18 22:41:49 +0000479 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000480
Akron5240b8c2016-05-20 09:17:41 +0200481 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200482 this._element.classList.add('visible');
Nils Diewald2fe12e12015-03-06 16:47:06 +0000483
Nils Diewald86dad5b2015-01-28 15:09:07 +0000484 // Add classes for rolling menus
485 this._boundary(true);
Akron6bb71582016-06-10 20:41:08 +0200486
Nils Diewald59c02fc2015-03-07 01:29:09 +0000487 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000488 },
489
Nils Diewald7148c6f2015-05-04 15:07:53 +0000490
491 /**
492 * Hide the menu and call the onHide callback.
493 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000494 hide : function () {
Akrona92fd8d2016-05-24 21:13:41 +0200495 this.removeItems();
Nils Diewald7148c6f2015-05-04 15:07:53 +0000496 this._prefix.clear();
Nils Diewald5c5a7472015-04-02 22:13:38 +0000497 this.onHide();
Akron6bb71582016-06-10 20:41:08 +0200498 this._element.classList.remove('visible');
499
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000500 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000501 },
502
Nils Diewald7148c6f2015-05-04 15:07:53 +0000503 /**
504 * Function released when the menu hides.
505 * This method is expected to be overridden.
506 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000507 onHide : function () {},
508
Nils Diewald7148c6f2015-05-04 15:07:53 +0000509
Nils Diewald86dad5b2015-01-28 15:09:07 +0000510 /**
511 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000512 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000513 */
Nils Diewald5975d702015-03-09 17:45:42 +0000514 prefix : function (pref) {
515 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200516 this._prefix.value(pref);
517 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000518 };
519 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000520 },
521
Akronc7448732016-04-27 14:06:58 +0200522 /**
523 * Get the lengthField object.
524 */
525 lengthField : function () {
526 return this._lengthField;
527 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000528
Akron5240b8c2016-05-20 09:17:41 +0200529 /**
530 * Get the associated slider object.
531 */
532 slider : function () {
533 return this._slider;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000534 },
535
Akron5240b8c2016-05-20 09:17:41 +0200536
Nils Diewald86dad5b2015-01-28 15:09:07 +0000537 /**
538 * Delete all visible items from the menu element
539 */
Akrona92fd8d2016-05-24 21:13:41 +0200540 removeItems : function () {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000541 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000542
Nils Diewald2fe12e12015-03-06 16:47:06 +0000543 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000544 var children = this._element.childNodes;
Akronc7448732016-04-27 14:06:58 +0200545 // Leave the prefix and lengthField
Akron9905e2a2016-05-10 16:06:44 +0200546 for (var i = children.length - 1; i >= 3; i--) {
Akrone4961b12017-05-10 21:04:46 +0200547 this._element.removeChild(
548 children[i]
549 );
Nils Diewald5975d702015-03-09 17:45:42 +0000550 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000551 },
552
Nils Diewald2fe12e12015-03-06 16:47:06 +0000553 /**
554 * Get a specific item from the complete list
555 *
556 * @param {number} index of the list item
557 */
558 item : function (index) {
559 return this._items[index]
560 },
561
562
Nils Diewald86dad5b2015-01-28 15:09:07 +0000563 /**
564 * Get a specific item from the filtered list
565 *
566 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000567 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000568 */
569 liveItem : function (index) {
570 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200571 if (!this._initList())
572 return;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000573
574 return this._items[this._list[index]];
575 },
576
Nils Diewald86dad5b2015-01-28 15:09:07 +0000577
578 /**
Akron5240b8c2016-05-20 09:17:41 +0200579 * Get a specific item from the viewport list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000580 *
581 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000582 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000583 */
584 shownItem : function (index) {
585 if (index >= this.limit())
Akrone4961b12017-05-10 21:04:46 +0200586 return;
Akron9c4d1ae2016-05-25 21:43:22 +0200587 return this.liveItem(this.offset + index);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000588 },
589
590
Nils Diewald2fe12e12015-03-06 16:47:06 +0000591 /**
Akron9c4d1ae2016-05-25 21:43:22 +0200592 * Get the length of the full item list
Nils Diewald2fe12e12015-03-06 16:47:06 +0000593 */
594 length : function () {
595 return this._items.length;
596 },
597
598
599 /**
Akron5240b8c2016-05-20 09:17:41 +0200600 * Length of the filtered item list.
601 */
602 liveLength : function () {
603 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200604 this._initList();
Akron5240b8c2016-05-20 09:17:41 +0200605 return this._list.length;
606 },
607
608
609 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000610 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000611 */
612 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000613
Akronb38afb22016-05-25 19:30:01 +0200614 // No list
615 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200616 return;
Akronb38afb22016-05-25 19:30:01 +0200617
Akron9c4d1ae2016-05-25 21:43:22 +0200618 // Deactivate old item
619 if (this.position !== -1 && !this._prefix.active()) {
Akrone4961b12017-05-10 21:04:46 +0200620 this.liveItem(this.position).active(false);
Akron9c4d1ae2016-05-25 21:43:22 +0200621 };
Nils Diewalde8518f82015-03-18 22:41:49 +0000622
Akron9c4d1ae2016-05-25 21:43:22 +0200623 // Get new active item
624 this.position++;
625 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000626
Nils Diewald5975d702015-03-09 17:45:42 +0000627 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000628 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000629
Akrone4961b12017-05-10 21:04:46 +0200630 // Activate prefix
631 var prefix = this._prefix;
Nils Diewald5975d702015-03-09 17:45:42 +0000632
Akrone4961b12017-05-10 21:04:46 +0200633 // Prefix is set and not active - choose!
634 if (prefix.isSet() && !prefix.active()) {
635 this.position--;
636 prefix.active(true);
637 return;
638 }
Akron9c4d1ae2016-05-25 21:43:22 +0200639
Akrone4961b12017-05-10 21:04:46 +0200640 // Choose first item
641 else {
642 newItem = this.liveItem(0);
643 // choose first item
644 this.position = 0;
645 this._showItems(0);
646 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000647 }
648
Akron5a1f5bb2016-05-23 22:00:39 +0200649 // The next element is after the viewport - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200650 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200651 this.screen(this.position - this.limit() + 1);
Akron5a1f5bb2016-05-23 22:00:39 +0200652 }
653
654 // The next element is before the viewport - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200655 else if (this.position <= this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200656 this.screen(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000657 };
Nils Diewald5975d702015-03-09 17:45:42 +0000658
659 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000660 newItem.active(true);
661 },
662
Nils Diewalde8518f82015-03-18 22:41:49 +0000663 /*
Nils Diewald86dad5b2015-01-28 15:09:07 +0000664 * Make the previous item in the menu active
665 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000666 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +0000667
Akronb38afb22016-05-25 19:30:01 +0200668 // No list
669 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200670 return;
Akronb38afb22016-05-25 19:30:01 +0200671
Akron9c4d1ae2016-05-25 21:43:22 +0200672 // Deactivate old item
Nils Diewald2d210752015-03-09 19:01:15 +0000673 if (!this._prefix.active()) {
Akron9c4d1ae2016-05-25 21:43:22 +0200674
Akrone4961b12017-05-10 21:04:46 +0200675 // No active element set
676 if (this.position === -1) {
677 this.position = this.liveLength();
678 }
Akron9c4d1ae2016-05-25 21:43:22 +0200679
Akrone4961b12017-05-10 21:04:46 +0200680 // No active element set
681 else {
682 this.liveItem(this.position--).active(false);
683 };
Nils Diewald2d210752015-03-09 19:01:15 +0000684 };
685
Akron9c4d1ae2016-05-25 21:43:22 +0200686 // Get new active item
687 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000688
689 // The previous element is undefined - roll to bottom
690 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000691
Akrone4961b12017-05-10 21:04:46 +0200692 // Activate prefix
693 var prefix = this._prefix;
694 var offset = this.liveLength() - this.limit();
695
696 // Normalize offset
697 offset = offset < 0 ? 0 : offset;
Nils Diewald2d210752015-03-09 19:01:15 +0000698
Akrone4961b12017-05-10 21:04:46 +0200699 // Choose the last item
700 this.position = this.liveLength() - 1;
701
702 // Prefix is set and not active - choose!
703 if (prefix.isSet() && !prefix.active()) {
704 this.position++;
705 prefix.active(true);
706 this.offset = offset;
707 return;
708 }
Nils Diewald2d210752015-03-09 19:01:15 +0000709
Akrone4961b12017-05-10 21:04:46 +0200710 // Choose last item
711 else {
712 newItem = this.liveItem(this.position);
713 this._showItems(offset);
714 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000715 }
716
Akron5a1f5bb2016-05-23 22:00:39 +0200717 // The previous element is before the view - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200718 else if (this.position < this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200719 this.screen(this.position);
Akron5a1f5bb2016-05-23 22:00:39 +0200720 }
721
722 // The previous element is after the view - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200723 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200724 this.screen(this.position - this.limit() + 2);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000725 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000726
Nils Diewald5975d702015-03-09 17:45:42 +0000727 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000728 newItem.active(true);
729 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000730
Akron3c2730f2016-05-24 15:08:29 +0200731 /**
732 * Move the page up by limit!
733 */
734 pageUp : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200735 this.screen(this.offset - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200736 },
737
738
739 /**
740 * Move the page down by limit!
741 */
742 pageDown : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200743 this.screen(this.offset + this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200744 },
745
Nils Diewald86dad5b2015-01-28 15:09:07 +0000746
Akron5240b8c2016-05-20 09:17:41 +0200747 // Unmark all items
748 _unmark : function () {
749 for (var i in this._list) {
Akrone4961b12017-05-10 21:04:46 +0200750 var item = this._items[this._list[i]];
751 item.lowlight();
752 item.active(false);
Akron5240b8c2016-05-20 09:17:41 +0200753 };
754 },
755
Akron5240b8c2016-05-20 09:17:41 +0200756 // Set boundary for viewport
757 _boundary : function (bool) {
758 this.item(this._list[0]).noMore(bool);
759 this.item(this._list[this._list.length - 1]).noMore(bool);
760 },
761
762
763 // Append Items that should be shown
764 _showItems : function (off) {
765
Akrona92fd8d2016-05-24 21:13:41 +0200766 // optimization: scroll down one step
Akron9c4d1ae2016-05-25 21:43:22 +0200767 if (this.offset === (off - 1)) {
Akrone4961b12017-05-10 21:04:46 +0200768 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200769
Akrone4961b12017-05-10 21:04:46 +0200770 // Remove the HTML node from the first item
771 // leave lengthField/prefix/slider
772 this._element.removeChild(this._element.children[3]);
773 var pos = this.offset + this.limit() - 1;
774 this._append(this._list[pos]);
Akrona92fd8d2016-05-24 21:13:41 +0200775 }
Akron5240b8c2016-05-20 09:17:41 +0200776
Akrona92fd8d2016-05-24 21:13:41 +0200777 // optimization: scroll up one step
Akron9c4d1ae2016-05-25 21:43:22 +0200778 else if (this.offset === (off + 1)) {
Akrone4961b12017-05-10 21:04:46 +0200779 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200780
Akrone4961b12017-05-10 21:04:46 +0200781 // Remove the HTML node from the last item
782 this._element.removeChild(this._element.lastChild);
Akron9c4d1ae2016-05-25 21:43:22 +0200783
Akrone4961b12017-05-10 21:04:46 +0200784 this._prepend(this._list[this.offset]);
Akrona92fd8d2016-05-24 21:13:41 +0200785 }
786 else {
Akrone4961b12017-05-10 21:04:46 +0200787 this.offset = off;
Akron5240b8c2016-05-20 09:17:41 +0200788
Akrone4961b12017-05-10 21:04:46 +0200789 // Remove all items
790 this.removeItems();
Akron5240b8c2016-05-20 09:17:41 +0200791
Akrone4961b12017-05-10 21:04:46 +0200792 // Use list
793 var shown = 0;
794 var i;
Akron5240b8c2016-05-20 09:17:41 +0200795
Akrone4961b12017-05-10 21:04:46 +0200796 for (i in this._list) {
Akrona92fd8d2016-05-24 21:13:41 +0200797
Akrone4961b12017-05-10 21:04:46 +0200798 // Don't show - it's before offset
799 shown++;
800 if (shown <= off)
801 continue;
Akrona92fd8d2016-05-24 21:13:41 +0200802
Akrone4961b12017-05-10 21:04:46 +0200803 var itemNr = this._list[i];
804 var item = this.item(itemNr);
805 this._append(itemNr);
806
807 if (shown >= (this.limit() + off))
808 break;
809 };
Akron5240b8c2016-05-20 09:17:41 +0200810 };
811
812 // set the slider to the new offset
Akron9c4d1ae2016-05-25 21:43:22 +0200813 this._slider.offset(this.offset);
Akron5240b8c2016-05-20 09:17:41 +0200814 },
815
816
817 // Append item to the shown list based on index
818 _append : function (i) {
819 var item = this.item(i);
820
821 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200822 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200823 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200824 };
825
Akron5240b8c2016-05-20 09:17:41 +0200826
827 // Append element
828 this.element().appendChild(item.element());
829 },
830
831
832 // Prepend item to the shown list based on index
833 _prepend : function (i) {
834 var item = this.item(i);
835
836 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200837 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200838 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200839 };
Akron5240b8c2016-05-20 09:17:41 +0200840
841 var e = this.element();
Akron9c4d1ae2016-05-25 21:43:22 +0200842
Akron5240b8c2016-05-20 09:17:41 +0200843 // Append element after lengthField/prefix/slider
844 e.insertBefore(
Akrone4961b12017-05-10 21:04:46 +0200845 item.element(),
846 e.children[3]
Akron5240b8c2016-05-20 09:17:41 +0200847 );
Nils Diewald2fe12e12015-03-06 16:47:06 +0000848 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000849 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000850});