blob: 490d2052f94eb42b699242d3d4b956d1c103c1a6 [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".
Akrone91da782017-12-15 17:17:50 +010016 * TODO: Support space separated list of prefixes so "co no" will highlight "common noun"
Nils Diewald2488d052015-04-09 21:46:02 +000017 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000018define([
19 'menu/item',
20 'menu/prefix',
Akronc7448732016-04-27 14:06:58 +020021 'menu/lengthField',
Akron9905e2a2016-05-10 16:06:44 +020022 'menu/slider',
Nils Diewald0e6992a2015-04-14 20:13:52 +000023 'util'
24], function (defaultItemClass,
Akrone4961b12017-05-10 21:04:46 +020025 defaultPrefixClass,
26 defaultLengthFieldClass,
27 sliderClass) {
Nils Diewaldfda29d92015-01-22 17:28:01 +000028
Nils Diewald0e6992a2015-04-14 20:13:52 +000029 // Default maximum number of menu items
30 var menuLimit = 8;
31
32 function _codeFromEvent (e) {
33 if (e.charCode && (e.keyCode == 0))
34 return e.charCode
35 return e.keyCode;
Nils Diewald59c02fc2015-03-07 01:29:09 +000036 };
37
Nils Diewald86dad5b2015-01-28 15:09:07 +000038
39 /**
40 * List of items for drop down menu (complete).
41 * Only a sublist of the menu is filtered (live).
42 * Only a sublist of the filtered menu is visible (shown).
43 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000044 return {
Nils Diewald86dad5b2015-01-28 15:09:07 +000045 /**
46 * Create new Menu based on the action prefix
47 * and a list of menu items.
48 *
Akron7524be12016-06-01 17:31:33 +020049 *
50 * Accepts an associative array containg the elements
51 * itemClass, prefixClass, lengthFieldClass
52 *
Nils Diewald86dad5b2015-01-28 15:09:07 +000053 * @this {Menu}
54 * @constructor
55 * @param {string} Context prefix
56 * @param {Array.<Array.<string>>} List of menu items
57 */
Akron7524be12016-06-01 17:31:33 +020058 create : function (list, params) {
59 return Object.create(this)._init(list, params);
Nils Diewald86dad5b2015-01-28 15:09:07 +000060 },
61
Akron5240b8c2016-05-20 09:17:41 +020062 // Initialize list
Akron7524be12016-06-01 17:31:33 +020063 _init : function (list, params) {
Akrona92fd8d2016-05-24 21:13:41 +020064
Akron7524be12016-06-01 17:31:33 +020065 if (params === undefined)
Akrone4961b12017-05-10 21:04:46 +020066 params = {};
Akron7524be12016-06-01 17:31:33 +020067
68 this._itemClass = params["itemClass"] || defaultItemClass;
Akron5240b8c2016-05-20 09:17:41 +020069
70 // Add prefix object
Akron7524be12016-06-01 17:31:33 +020071 if (params["prefixClass"] !== undefined) {
Akrone4961b12017-05-10 21:04:46 +020072 this._prefix = params["prefixClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020073 }
74 else {
Akrone4961b12017-05-10 21:04:46 +020075 this._prefix = defaultPrefixClass.create();
Akron5240b8c2016-05-20 09:17:41 +020076 };
77 this._prefix._menu = this;
78
79 // Add lengthField object
Akron7524be12016-06-01 17:31:33 +020080 if (params["lengthFieldClass"] !== undefined) {
Akrone4961b12017-05-10 21:04:46 +020081 this._lengthField = params["lengthFieldClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020082 }
83 else {
Akrone4961b12017-05-10 21:04:46 +020084 this._lengthField = defaultLengthFieldClass.create();
Akron5240b8c2016-05-20 09:17:41 +020085 };
86 this._lengthField._menu = this;
87
88 // Initialize slider
89 this._slider = sliderClass.create(this);
90
91 // Create the element
Akron9c4d1ae2016-05-25 21:43:22 +020092 var el = document.createElement("ul");
93 with (el) {
Akrone4961b12017-05-10 21:04:46 +020094 style.outline = 0;
95 setAttribute('tabindex', 0);
96 classList.add('menu', 'roll');
97 appendChild(this._prefix.element());
98 appendChild(this._lengthField.element());
99 appendChild(this._slider.element());
Akron9c4d1ae2016-05-25 21:43:22 +0200100 };
Akron5240b8c2016-05-20 09:17:41 +0200101
102 // This has to be cleaned up later on
Akron9c4d1ae2016-05-25 21:43:22 +0200103 el["menu"] = this;
Akron5240b8c2016-05-20 09:17:41 +0200104
105 // Arrow keys
Akron9c4d1ae2016-05-25 21:43:22 +0200106 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200107 'keydown',
108 this._keydown.bind(this),
109 false
Akron5240b8c2016-05-20 09:17:41 +0200110 );
111
112 // Strings
Akron9c4d1ae2016-05-25 21:43:22 +0200113 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200114 'keypress',
115 this._keypress.bind(this),
116 false
Akron5240b8c2016-05-20 09:17:41 +0200117 );
118
119 // Mousewheel
Akron9c4d1ae2016-05-25 21:43:22 +0200120 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200121 'wheel',
122 this._mousewheel.bind(this),
123 false
Akron5240b8c2016-05-20 09:17:41 +0200124 );
Akron9c4d1ae2016-05-25 21:43:22 +0200125 this._element = el;
Akrone4961b12017-05-10 21:04:46 +0200126
Akron5240b8c2016-05-20 09:17:41 +0200127 this._items = new Array();
Akron5240b8c2016-05-20 09:17:41 +0200128
Akron9c4d1ae2016-05-25 21:43:22 +0200129 var i = 0;
Akron5240b8c2016-05-20 09:17:41 +0200130 // Initialize item list based on parameters
Akron7524be12016-06-01 17:31:33 +0200131 for (i in list) {
Akrone4961b12017-05-10 21:04:46 +0200132 var obj = this._itemClass.create(list[i]);
Akron5240b8c2016-05-20 09:17:41 +0200133
Akrone4961b12017-05-10 21:04:46 +0200134 // This may become circular
135 obj["_menu"] = this;
136 this._lengthField.add(list[i]);
137 this._items.push(obj);
Akron5240b8c2016-05-20 09:17:41 +0200138 };
139
Akron9c4d1ae2016-05-25 21:43:22 +0200140 this._limit = menuLimit;
141 this._slider.length(this.liveLength())
Akrone4961b12017-05-10 21:04:46 +0200142 .limit(this._limit)
143 .reInit();
144
Akron5240b8c2016-05-20 09:17:41 +0200145 this._firstActive = false; // Show the first item active always?
Akron9c4d1ae2016-05-25 21:43:22 +0200146 this.offset = 0;
147 this.position = 0;
Akron5240b8c2016-05-20 09:17:41 +0200148 return this;
149 },
150
151 // Initialize the item list
152 _initList : function () {
153
154 // Create a new list
155 if (this._list === undefined) {
Akrone4961b12017-05-10 21:04:46 +0200156 this._list = [];
Akron5240b8c2016-05-20 09:17:41 +0200157 }
158 else if (this._list.length !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200159 this._boundary(false);
160 this._list.length = 0;
Akron5240b8c2016-05-20 09:17:41 +0200161 };
162
163 // Offset is initially zero
Akron9c4d1ae2016-05-25 21:43:22 +0200164 this.offset = 0;
Akron5240b8c2016-05-20 09:17:41 +0200165
166 // There is no prefix set
167 if (this.prefix().length <= 0) {
168
Akrone4961b12017-05-10 21:04:46 +0200169 // add all items to the list and lowlight
170 var i = 0;
171 for (; i < this._items.length; i++) {
172 this._list.push(i);
173 this._items[i].lowlight();
174 };
Akron5240b8c2016-05-20 09:17:41 +0200175
Akrone4961b12017-05-10 21:04:46 +0200176 this._slider.length(i).reInit();;
Akron97752a72016-05-25 14:43:07 +0200177
Akrone4961b12017-05-10 21:04:46 +0200178 return true;
Akron5240b8c2016-05-20 09:17:41 +0200179 };
180
181 /*
182 * There is a prefix set, so filter the list!
183 */
184 var pos;
Akronacffc652017-12-18 21:21:25 +0100185 var prefixList = this.prefix().toLowerCase().split(" ");
186
187 var items = [];
188 var maxPoints = 1; // minimum 1
Akron5240b8c2016-05-20 09:17:41 +0200189
190 // Iterate over all items and choose preferred matching items
191 // i.e. the matching happens at the word start
192 for (pos = 0; pos < this._items.length; pos++) {
Akronacffc652017-12-18 21:21:25 +0100193
194 var points = 0;
195
196 for (pref = 0; pref < prefixList.length; pref++) {
197 var prefix = " " + prefixList[pref];
198
199 // Check if it matches at the beginning
200 if ((this.item(pos).lcField().indexOf(prefix)) >= 0) {
201 points += 5;
202 }
203
204 // Check if it matches anywhere
205 else if ((this.item(pos).lcField().indexOf(prefix.substring(1))) >= 0) {
206 points += 1;
207 };
208 };
209
210 if (points > maxPoints) {
211 this._list = [pos];
212 maxPoints = points;
213 }
214 else if (points == maxPoints) {
Akrone4961b12017-05-10 21:04:46 +0200215 this._list.push(pos);
Akronacffc652017-12-18 21:21:25 +0100216 }
Akron5240b8c2016-05-20 09:17:41 +0200217 };
218
219 // The list is empty - so lower your expectations
220 // Iterate over all items and choose matching items
221 // i.e. the matching happens anywhere in the word
Akronacffc652017-12-18 21:21:25 +0100222 /*
Akron6ffad5d2016-05-24 17:16:58 +0200223 prefix = prefix.substring(1);
Akron5240b8c2016-05-20 09:17:41 +0200224 if (this._list.length == 0) {
Akrone4961b12017-05-10 21:04:46 +0200225 for (pos = 0; pos < this._items.length; pos++) {
226 if ((this.item(pos).lcField().indexOf(prefix)) >= 0)
227 this._list.push(pos);
228 };
Akron5240b8c2016-05-20 09:17:41 +0200229 };
Akronacffc652017-12-18 21:21:25 +0100230 */
Akron5240b8c2016-05-20 09:17:41 +0200231
Akron9c4d1ae2016-05-25 21:43:22 +0200232 this._slider.length(this._list.length).reInit();
Akron6ed13992016-05-23 18:06:05 +0200233
Akron5240b8c2016-05-20 09:17:41 +0200234 // Filter was successful - yeah!
235 return this._list.length > 0 ? true : false;
236 },
237
238
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000239 /**
240 * Destroy this menu
241 * (in case you don't trust the
242 * mark and sweep GC)!
243 */
244 destroy : function () {
Akron47c086c2016-05-18 21:22:06 +0200245
Akron5240b8c2016-05-20 09:17:41 +0200246 // Remove circular reference to "this" in menu
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000247 if (this._element != undefined)
Akrone4961b12017-05-10 21:04:46 +0200248 delete this._element["menu"];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000249
Akron5240b8c2016-05-20 09:17:41 +0200250 // Remove circular reference to "this" in items
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000251 for (var i = 0; i < this._items.length; i++) {
Akrone4961b12017-05-10 21:04:46 +0200252 delete this._items[i]["_menu"];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000253 };
Akron5240b8c2016-05-20 09:17:41 +0200254
255 // Remove circular reference to "this" in prefix
Nils Diewald5c5a7472015-04-02 22:13:38 +0000256 delete this._prefix['_menu'];
Akron5240b8c2016-05-20 09:17:41 +0200257 delete this._lengthField['_menu'];
258 delete this._slider['_menu'];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000259 },
260
Nils Diewald7148c6f2015-05-04 15:07:53 +0000261
262 /**
263 * Focus on this menu.
264 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000265 focus : function () {
266 this._element.focus();
267 },
268
Nils Diewald7148c6f2015-05-04 15:07:53 +0000269
Nils Diewald59c02fc2015-03-07 01:29:09 +0000270 // mouse wheel treatment
271 _mousewheel : function (e) {
272 var delta = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000273
274 delta = e.deltaY / 120;
275 if (delta > 0)
Akrone4961b12017-05-10 21:04:46 +0200276 this.next();
Nils Diewald5975d702015-03-09 17:45:42 +0000277 else if (delta < 0)
Akrone4961b12017-05-10 21:04:46 +0200278 this.prev();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000279 e.halt();
280 },
281
Nils Diewald7148c6f2015-05-04 15:07:53 +0000282
Nils Diewald59c02fc2015-03-07 01:29:09 +0000283 // Arrow key and prefix treatment
Nils Diewald47f366b2015-04-15 20:06:35 +0000284 _keydown : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000285 var code = _codeFromEvent(e);
286
Nils Diewald59c02fc2015-03-07 01:29:09 +0000287 switch (code) {
288 case 27: // 'Esc'
Akrone4961b12017-05-10 21:04:46 +0200289 e.halt();
290 this.hide();
291 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000292
Nils Diewald59c02fc2015-03-07 01:29:09 +0000293 case 38: // 'Up'
Akrone4961b12017-05-10 21:04:46 +0200294 e.halt();
295 this.prev();
296 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000297 case 33: // 'Page up'
Akrone4961b12017-05-10 21:04:46 +0200298 e.halt();
299 this.pageUp();
300 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000301 case 40: // 'Down'
Akrone4961b12017-05-10 21:04:46 +0200302 e.halt();
303 this.next();
304 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000305 case 34: // 'Page down'
Akrone4961b12017-05-10 21:04:46 +0200306 e.halt();
307 this.pageDown();
308 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000309 case 39: // 'Right'
Akrone4961b12017-05-10 21:04:46 +0200310 if (this._prefix.active())
311 break;
Nils Diewalde8518f82015-03-18 22:41:49 +0000312
Akrone4961b12017-05-10 21:04:46 +0200313 var item = this.liveItem(this.position);
314
315 if (item["further"] !== undefined) {
316 item["further"].bind(item).apply();
317 };
318
319 e.halt();
320 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000321 case 13: // 'Enter'
322
Akrone4961b12017-05-10 21:04:46 +0200323 // Click on prefix
324 if (this._prefix.active())
325 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000326
Akrone4961b12017-05-10 21:04:46 +0200327 // Click on item
328 else
329 this.liveItem(this.position).onclick(e);
330 e.halt();
331 break;
Nils Diewald59c02fc2015-03-07 01:29:09 +0000332 case 8: // 'Backspace'
Akrone4961b12017-05-10 21:04:46 +0200333 this._prefix.chop();
334 this.show();
335 e.halt();
336 break;
Nils Diewald47f366b2015-04-15 20:06:35 +0000337 };
338 },
Nils Diewald59c02fc2015-03-07 01:29:09 +0000339
Nils Diewald47f366b2015-04-15 20:06:35 +0000340 // Add characters to prefix
341 _keypress : function (e) {
Akron9c2f9382016-05-25 16:36:04 +0200342 if (e.charCode !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200343 e.halt();
344 var c = String.fromCharCode(_codeFromEvent(e));
345
346 // Add prefix
347 this._prefix.add(c);
348 this.show();
Akron9c2f9382016-05-25 16:36:04 +0200349 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000350 },
351
Akron47c086c2016-05-18 21:22:06 +0200352 /**
Akron5240b8c2016-05-20 09:17:41 +0200353 * Show a screen with a given offset
354 * in the viewport.
Akron47c086c2016-05-18 21:22:06 +0200355 */
356 screen : function (nr) {
Akron3c2730f2016-05-24 15:08:29 +0200357 if (nr < 0) {
Akrone4961b12017-05-10 21:04:46 +0200358 nr = 0
Akron3c2730f2016-05-24 15:08:29 +0200359 }
Akron6ac58442016-05-24 16:52:29 +0200360 else if (nr > (this.liveLength() - this.limit())) {
Akrone4961b12017-05-10 21:04:46 +0200361 nr = (this.liveLength() - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200362 };
363
Akron9c4d1ae2016-05-25 21:43:22 +0200364 if (this.offset === nr)
Akrone4961b12017-05-10 21:04:46 +0200365 return;
Akron5a1f5bb2016-05-23 22:00:39 +0200366
Akron47c086c2016-05-18 21:22:06 +0200367 this._showItems(nr);
368 },
369
Nils Diewald2fe12e12015-03-06 16:47:06 +0000370 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000371 * Get the associated dom element.
Nils Diewald2fe12e12015-03-06 16:47:06 +0000372 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000373 element : function () {
374 return this._element;
375 },
376
Nils Diewald2fe12e12015-03-06 16:47:06 +0000377 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000378 * Get the creator class for items
Nils Diewald2fe12e12015-03-06 16:47:06 +0000379 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000380 itemClass : function () {
381 return this._itemClass;
382 },
383
384 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000385 * Get and set the numerical value
386 * for the maximum number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000387 */
388 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000389 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200390 if (this._limit !== limit) {
391 this._limit = limit;
392 this._slider.limit(limit).reInit();
393 };
394 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000395 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000396 return this._limit;
397 },
398
Nils Diewald7148c6f2015-05-04 15:07:53 +0000399
Nils Diewald86dad5b2015-01-28 15:09:07 +0000400 /**
401 * Upgrade this object to another object,
402 * while private data stays intact.
403 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000404 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000405 */
406 upgradeTo : function (props) {
407 for (var prop in props) {
Akrone4961b12017-05-10 21:04:46 +0200408 this[prop] = props[prop];
Nils Diewald86dad5b2015-01-28 15:09:07 +0000409 };
410 return this;
411 },
412
Nils Diewald7148c6f2015-05-04 15:07:53 +0000413
Nils Diewald86dad5b2015-01-28 15:09:07 +0000414 /**
Akron97752a72016-05-25 14:43:07 +0200415 * Filter the list and make it visible.
416 * This is always called once the prefix changes.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000417 *
418 * @param {string} Prefix for filtering the list
419 */
Akron6ed13992016-05-23 18:06:05 +0200420 show : function (active) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000421
Akron5240b8c2016-05-20 09:17:41 +0200422 // show menu based on initial offset
Akron6ac58442016-05-24 16:52:29 +0200423 this._unmark(); // Unmark everything that was marked before
Akrona92fd8d2016-05-24 21:13:41 +0200424 this.removeItems();
Akron6ed13992016-05-23 18:06:05 +0200425
426 // Initialize the list
427 if (!this._initList()) {
Akron6ac58442016-05-24 16:52:29 +0200428
Akrone4961b12017-05-10 21:04:46 +0200429 // The prefix is not active
430 this._prefix.active(true);
Akron6ed13992016-05-23 18:06:05 +0200431
Akrone4961b12017-05-10 21:04:46 +0200432 // finally show the element
433 this._element.classList.add('visible');
434
435 return true;
Akron6ed13992016-05-23 18:06:05 +0200436 };
437
438 var offset = 0;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000439
Akron9c2f9382016-05-25 16:36:04 +0200440 // Set a chosen value to active and move the viewport
Akron6ed13992016-05-23 18:06:05 +0200441 if (arguments.length === 1) {
442
Akrone4961b12017-05-10 21:04:46 +0200443 // Normalize active value
444 if (active < 0) {
445 active = 0;
446 }
447 else if (active >= this.liveLength()) {
448 active = this.liveLength() - 1;
449 };
Akron6ed13992016-05-23 18:06:05 +0200450
Akrone4961b12017-05-10 21:04:46 +0200451 // Item is outside the first viewport
452 if (active >= this._limit) {
453 offset = active;
454 if (offset > (this.liveLength() - this._limit)) {
455 offset = this.liveLength() - this._limit;
456 };
457 };
458
459 this.position = active;
Akron6ed13992016-05-23 18:06:05 +0200460 }
461
Akron9c2f9382016-05-25 16:36:04 +0200462 // Choose the first item
Akron6ed13992016-05-23 18:06:05 +0200463 else if (this._firstActive) {
Akrone4961b12017-05-10 21:04:46 +0200464 this.position = 0;
Akron47c086c2016-05-18 21:22:06 +0200465 }
Akroncb351d62016-05-19 23:10:33 +0200466
Akron9c2f9382016-05-25 16:36:04 +0200467 // Choose no item
Akron47c086c2016-05-18 21:22:06 +0200468 else {
Akrone4961b12017-05-10 21:04:46 +0200469 this.position = -1;
Akron6ed13992016-05-23 18:06:05 +0200470 };
471
Akron9c4d1ae2016-05-25 21:43:22 +0200472 this.offset = offset;
Akron6ed13992016-05-23 18:06:05 +0200473 this._showItems(offset); // Show new item list
474
475 // Make chosen value active
476 if (this.position !== -1) {
Akrone4961b12017-05-10 21:04:46 +0200477 this.liveItem(this.position).active(true);
Akron6ed13992016-05-23 18:06:05 +0200478 };
Akron37513a62015-11-17 01:07:11 +0100479
Akron5240b8c2016-05-20 09:17:41 +0200480 // The prefix is not active
Nils Diewalde8518f82015-03-18 22:41:49 +0000481 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000482
Akron5240b8c2016-05-20 09:17:41 +0200483 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200484 this._element.classList.add('visible');
Nils Diewald2fe12e12015-03-06 16:47:06 +0000485
Nils Diewald86dad5b2015-01-28 15:09:07 +0000486 // Add classes for rolling menus
487 this._boundary(true);
Akron6bb71582016-06-10 20:41:08 +0200488
Nils Diewald59c02fc2015-03-07 01:29:09 +0000489 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000490 },
491
Nils Diewald7148c6f2015-05-04 15:07:53 +0000492
493 /**
494 * Hide the menu and call the onHide callback.
495 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000496 hide : function () {
Akrona92fd8d2016-05-24 21:13:41 +0200497 this.removeItems();
Nils Diewald7148c6f2015-05-04 15:07:53 +0000498 this._prefix.clear();
Nils Diewald5c5a7472015-04-02 22:13:38 +0000499 this.onHide();
Akron6bb71582016-06-10 20:41:08 +0200500 this._element.classList.remove('visible');
501
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000502 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000503 },
504
Nils Diewald7148c6f2015-05-04 15:07:53 +0000505 /**
506 * Function released when the menu hides.
507 * This method is expected to be overridden.
508 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000509 onHide : function () {},
510
Nils Diewald7148c6f2015-05-04 15:07:53 +0000511
Nils Diewald86dad5b2015-01-28 15:09:07 +0000512 /**
513 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000514 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000515 */
Nils Diewald5975d702015-03-09 17:45:42 +0000516 prefix : function (pref) {
517 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200518 this._prefix.value(pref);
519 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000520 };
521 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000522 },
523
Akronc7448732016-04-27 14:06:58 +0200524 /**
525 * Get the lengthField object.
526 */
527 lengthField : function () {
528 return this._lengthField;
529 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000530
Akron5240b8c2016-05-20 09:17:41 +0200531 /**
532 * Get the associated slider object.
533 */
534 slider : function () {
535 return this._slider;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000536 },
537
Akron5240b8c2016-05-20 09:17:41 +0200538
Nils Diewald86dad5b2015-01-28 15:09:07 +0000539 /**
540 * Delete all visible items from the menu element
541 */
Akrona92fd8d2016-05-24 21:13:41 +0200542 removeItems : function () {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000543 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000544
Nils Diewald2fe12e12015-03-06 16:47:06 +0000545 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000546 var children = this._element.childNodes;
Akronc7448732016-04-27 14:06:58 +0200547 // Leave the prefix and lengthField
Akron9905e2a2016-05-10 16:06:44 +0200548 for (var i = children.length - 1; i >= 3; i--) {
Akrone4961b12017-05-10 21:04:46 +0200549 this._element.removeChild(
550 children[i]
551 );
Nils Diewald5975d702015-03-09 17:45:42 +0000552 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000553 },
554
Nils Diewald2fe12e12015-03-06 16:47:06 +0000555 /**
556 * Get a specific item from the complete list
557 *
558 * @param {number} index of the list item
559 */
560 item : function (index) {
561 return this._items[index]
562 },
563
564
Nils Diewald86dad5b2015-01-28 15:09:07 +0000565 /**
566 * Get a specific item from the filtered list
567 *
568 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000569 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000570 */
571 liveItem : function (index) {
572 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200573 if (!this._initList())
574 return;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000575
576 return this._items[this._list[index]];
577 },
578
Nils Diewald86dad5b2015-01-28 15:09:07 +0000579
580 /**
Akron5240b8c2016-05-20 09:17:41 +0200581 * Get a specific item from the viewport list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000582 *
583 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000584 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000585 */
586 shownItem : function (index) {
587 if (index >= this.limit())
Akrone4961b12017-05-10 21:04:46 +0200588 return;
Akron9c4d1ae2016-05-25 21:43:22 +0200589 return this.liveItem(this.offset + index);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000590 },
591
592
Nils Diewald2fe12e12015-03-06 16:47:06 +0000593 /**
Akron9c4d1ae2016-05-25 21:43:22 +0200594 * Get the length of the full item list
Nils Diewald2fe12e12015-03-06 16:47:06 +0000595 */
596 length : function () {
597 return this._items.length;
598 },
599
600
601 /**
Akron5240b8c2016-05-20 09:17:41 +0200602 * Length of the filtered item list.
603 */
604 liveLength : function () {
605 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200606 this._initList();
Akron5240b8c2016-05-20 09:17:41 +0200607 return this._list.length;
608 },
609
610
611 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000612 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000613 */
614 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000615
Akronb38afb22016-05-25 19:30:01 +0200616 // No list
617 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200618 return;
Akronb38afb22016-05-25 19:30:01 +0200619
Akron9c4d1ae2016-05-25 21:43:22 +0200620 // Deactivate old item
621 if (this.position !== -1 && !this._prefix.active()) {
Akrone4961b12017-05-10 21:04:46 +0200622 this.liveItem(this.position).active(false);
Akron9c4d1ae2016-05-25 21:43:22 +0200623 };
Nils Diewalde8518f82015-03-18 22:41:49 +0000624
Akron9c4d1ae2016-05-25 21:43:22 +0200625 // Get new active item
626 this.position++;
627 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000628
Nils Diewald5975d702015-03-09 17:45:42 +0000629 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000630 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000631
Akrone4961b12017-05-10 21:04:46 +0200632 // Activate prefix
633 var prefix = this._prefix;
Nils Diewald5975d702015-03-09 17:45:42 +0000634
Akrone4961b12017-05-10 21:04:46 +0200635 // Prefix is set and not active - choose!
636 if (prefix.isSet() && !prefix.active()) {
637 this.position--;
638 prefix.active(true);
639 return;
640 }
Akron9c4d1ae2016-05-25 21:43:22 +0200641
Akrone4961b12017-05-10 21:04:46 +0200642 // Choose first item
643 else {
644 newItem = this.liveItem(0);
645 // choose first item
646 this.position = 0;
647 this._showItems(0);
648 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000649 }
650
Akron5a1f5bb2016-05-23 22:00:39 +0200651 // The next element is after the viewport - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200652 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200653 this.screen(this.position - this.limit() + 1);
Akron5a1f5bb2016-05-23 22:00:39 +0200654 }
655
656 // The next element is before the viewport - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200657 else if (this.position <= this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200658 this.screen(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000659 };
Nils Diewald5975d702015-03-09 17:45:42 +0000660
661 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000662 newItem.active(true);
663 },
664
Nils Diewalde8518f82015-03-18 22:41:49 +0000665 /*
Nils Diewald86dad5b2015-01-28 15:09:07 +0000666 * Make the previous item in the menu active
667 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000668 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +0000669
Akronb38afb22016-05-25 19:30:01 +0200670 // No list
671 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200672 return;
Akronb38afb22016-05-25 19:30:01 +0200673
Akron9c4d1ae2016-05-25 21:43:22 +0200674 // Deactivate old item
Nils Diewald2d210752015-03-09 19:01:15 +0000675 if (!this._prefix.active()) {
Akron9c4d1ae2016-05-25 21:43:22 +0200676
Akrone4961b12017-05-10 21:04:46 +0200677 // No active element set
678 if (this.position === -1) {
679 this.position = this.liveLength();
680 }
Akron9c4d1ae2016-05-25 21:43:22 +0200681
Akrone4961b12017-05-10 21:04:46 +0200682 // No active element set
683 else {
684 this.liveItem(this.position--).active(false);
685 };
Nils Diewald2d210752015-03-09 19:01:15 +0000686 };
687
Akron9c4d1ae2016-05-25 21:43:22 +0200688 // Get new active item
689 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000690
691 // The previous element is undefined - roll to bottom
692 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000693
Akrone4961b12017-05-10 21:04:46 +0200694 // Activate prefix
695 var prefix = this._prefix;
696 var offset = this.liveLength() - this.limit();
697
698 // Normalize offset
699 offset = offset < 0 ? 0 : offset;
Nils Diewald2d210752015-03-09 19:01:15 +0000700
Akrone4961b12017-05-10 21:04:46 +0200701 // Choose the last item
702 this.position = this.liveLength() - 1;
703
704 // Prefix is set and not active - choose!
705 if (prefix.isSet() && !prefix.active()) {
706 this.position++;
707 prefix.active(true);
708 this.offset = offset;
709 return;
710 }
Nils Diewald2d210752015-03-09 19:01:15 +0000711
Akrone4961b12017-05-10 21:04:46 +0200712 // Choose last item
713 else {
714 newItem = this.liveItem(this.position);
715 this._showItems(offset);
716 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000717 }
718
Akron5a1f5bb2016-05-23 22:00:39 +0200719 // The previous element is before the view - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200720 else if (this.position < this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200721 this.screen(this.position);
Akron5a1f5bb2016-05-23 22:00:39 +0200722 }
723
724 // The previous element is after the view - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200725 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200726 this.screen(this.position - this.limit() + 2);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000727 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000728
Nils Diewald5975d702015-03-09 17:45:42 +0000729 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000730 newItem.active(true);
731 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000732
Akron3c2730f2016-05-24 15:08:29 +0200733 /**
734 * Move the page up by limit!
735 */
736 pageUp : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200737 this.screen(this.offset - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200738 },
739
740
741 /**
742 * Move the page down by limit!
743 */
744 pageDown : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200745 this.screen(this.offset + this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200746 },
747
Nils Diewald86dad5b2015-01-28 15:09:07 +0000748
Akron5240b8c2016-05-20 09:17:41 +0200749 // Unmark all items
750 _unmark : function () {
751 for (var i in this._list) {
Akrone4961b12017-05-10 21:04:46 +0200752 var item = this._items[this._list[i]];
753 item.lowlight();
754 item.active(false);
Akron5240b8c2016-05-20 09:17:41 +0200755 };
756 },
757
Akron5240b8c2016-05-20 09:17:41 +0200758 // Set boundary for viewport
759 _boundary : function (bool) {
760 this.item(this._list[0]).noMore(bool);
761 this.item(this._list[this._list.length - 1]).noMore(bool);
762 },
763
764
765 // Append Items that should be shown
766 _showItems : function (off) {
767
Akrona92fd8d2016-05-24 21:13:41 +0200768 // optimization: scroll down one step
Akron9c4d1ae2016-05-25 21:43:22 +0200769 if (this.offset === (off - 1)) {
Akrone4961b12017-05-10 21:04:46 +0200770 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200771
Akrone4961b12017-05-10 21:04:46 +0200772 // Remove the HTML node from the first item
773 // leave lengthField/prefix/slider
774 this._element.removeChild(this._element.children[3]);
775 var pos = this.offset + this.limit() - 1;
776 this._append(this._list[pos]);
Akrona92fd8d2016-05-24 21:13:41 +0200777 }
Akron5240b8c2016-05-20 09:17:41 +0200778
Akrona92fd8d2016-05-24 21:13:41 +0200779 // optimization: scroll up one step
Akron9c4d1ae2016-05-25 21:43:22 +0200780 else if (this.offset === (off + 1)) {
Akrone4961b12017-05-10 21:04:46 +0200781 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200782
Akrone4961b12017-05-10 21:04:46 +0200783 // Remove the HTML node from the last item
784 this._element.removeChild(this._element.lastChild);
Akron9c4d1ae2016-05-25 21:43:22 +0200785
Akrone4961b12017-05-10 21:04:46 +0200786 this._prepend(this._list[this.offset]);
Akrona92fd8d2016-05-24 21:13:41 +0200787 }
788 else {
Akrone4961b12017-05-10 21:04:46 +0200789 this.offset = off;
Akron5240b8c2016-05-20 09:17:41 +0200790
Akrone4961b12017-05-10 21:04:46 +0200791 // Remove all items
792 this.removeItems();
Akron5240b8c2016-05-20 09:17:41 +0200793
Akrone4961b12017-05-10 21:04:46 +0200794 // Use list
795 var shown = 0;
796 var i;
Akron5240b8c2016-05-20 09:17:41 +0200797
Akrone4961b12017-05-10 21:04:46 +0200798 for (i in this._list) {
Akrona92fd8d2016-05-24 21:13:41 +0200799
Akrone4961b12017-05-10 21:04:46 +0200800 // Don't show - it's before offset
801 shown++;
802 if (shown <= off)
803 continue;
Akrona92fd8d2016-05-24 21:13:41 +0200804
Akrone4961b12017-05-10 21:04:46 +0200805 var itemNr = this._list[i];
806 var item = this.item(itemNr);
807 this._append(itemNr);
808
809 if (shown >= (this.limit() + off))
810 break;
811 };
Akron5240b8c2016-05-20 09:17:41 +0200812 };
813
814 // set the slider to the new offset
Akron9c4d1ae2016-05-25 21:43:22 +0200815 this._slider.offset(this.offset);
Akron5240b8c2016-05-20 09:17:41 +0200816 },
817
818
819 // Append item to the shown list based on index
820 _append : function (i) {
821 var item = this.item(i);
822
823 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200824 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200825 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200826 };
827
Akron5240b8c2016-05-20 09:17:41 +0200828
829 // Append element
830 this.element().appendChild(item.element());
831 },
832
833
834 // Prepend item to the shown list based on index
835 _prepend : function (i) {
836 var item = this.item(i);
837
838 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200839 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200840 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200841 };
Akron5240b8c2016-05-20 09:17:41 +0200842
843 var e = this.element();
Akron9c4d1ae2016-05-25 21:43:22 +0200844
Akron5240b8c2016-05-20 09:17:41 +0200845 // Append element after lengthField/prefix/slider
846 e.insertBefore(
Akrone4961b12017-05-10 21:04:46 +0200847 item.element(),
848 e.children[3]
Akron5240b8c2016-05-20 09:17:41 +0200849 );
Nils Diewald2fe12e12015-03-06 16:47:06 +0000850 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000851 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000852});