blob: a7271c5704a4c8db67175112545438374b570f04 [file] [log] [blame]
Nils Diewaldfda29d92015-01-22 17:28:01 +00001var KorAP = KorAP || {};
2
Nils Diewald2fe12e12015-03-06 16:47:06 +00003/**
4 * Create scrollable drop-down menus.
5 *
6 * @author Nils Diewald
7 */
8
Nils Diewaldfda29d92015-01-22 17:28:01 +00009(function (KorAP) {
10 "use strict";
11
Nils Diewald59c02fc2015-03-07 01:29:09 +000012 // Don't let events bubble up
Nils Diewaldd2b57372015-03-10 20:09:48 +000013 if (Event.halt === undefined) {
14 // Don't let events bubble up
15 Event.prototype.halt = function () {
16 this.stopPropagation();
17 this.preventDefault();
18 };
Nils Diewald59c02fc2015-03-07 01:29:09 +000019 };
20
Nils Diewald86dad5b2015-01-28 15:09:07 +000021 // Default maximum number of menu items
22 KorAP.menuLimit = 8;
23
24 /**
25 * List of items for drop down menu (complete).
26 * Only a sublist of the menu is filtered (live).
27 * Only a sublist of the filtered menu is visible (shown).
28 */
29 KorAP.Menu = {
30 /**
31 * Create new Menu based on the action prefix
32 * and a list of menu items.
33 *
34 * @this {Menu}
35 * @constructor
36 * @param {string} Context prefix
37 * @param {Array.<Array.<string>>} List of menu items
38 */
39 create : function (params) {
40 return Object.create(KorAP.Menu)._init(params);
41 },
42
Nils Diewald6e43ffd2015-03-25 18:55:39 +000043 /**
44 * Destroy this menu
45 * (in case you don't trust the
46 * mark and sweep GC)!
47 */
48 destroy : function () {
49 if (this._element != undefined)
50 delete this._element["menu"];
51
52 for (var i = 0; i < this._items.length; i++) {
53 delete this._items[i]["_menu"];
54 };
55
56 },
57
Nils Diewald2fe12e12015-03-06 16:47:06 +000058 focus : function () {
59 this._element.focus();
60 },
61
Nils Diewald59c02fc2015-03-07 01:29:09 +000062 // mouse wheel treatment
63 _mousewheel : function (e) {
64 var delta = 0;
Nils Diewald5975d702015-03-09 17:45:42 +000065
66 delta = e.deltaY / 120;
67 if (delta > 0)
Nils Diewald59c02fc2015-03-07 01:29:09 +000068 this.next();
Nils Diewald5975d702015-03-09 17:45:42 +000069 else if (delta < 0)
Nils Diewald59c02fc2015-03-07 01:29:09 +000070 this.prev();
Nils Diewald59c02fc2015-03-07 01:29:09 +000071 e.halt();
72 },
73
74 // Arrow key and prefix treatment
Nils Diewald5975d702015-03-09 17:45:42 +000075 _keypress : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +000076 var code = _codeFromEvent(e);
77
Nils Diewald59c02fc2015-03-07 01:29:09 +000078 switch (code) {
79 case 27: // 'Esc'
80 e.halt();
81 this.hide();
82 break;
Nils Diewald5975d702015-03-09 17:45:42 +000083
Nils Diewald59c02fc2015-03-07 01:29:09 +000084 case 38: // 'Up'
85 e.halt();
86 this.prev();
87 break;
Nils Diewald5975d702015-03-09 17:45:42 +000088 case 33: // 'Page up'
Nils Diewald59c02fc2015-03-07 01:29:09 +000089 e.halt();
Nils Diewald5975d702015-03-09 17:45:42 +000090 this.prev();
91 break;
92 case 40: // 'Down'
93 e.halt();
94 this.next();
95 break;
96 case 34: // 'Page down'
97 e.halt();
98 this.next();
99 break;
100 case 39: // 'Right'
Nils Diewalde8518f82015-03-18 22:41:49 +0000101 if (this._prefix.active())
102 break;
103
Nils Diewald5975d702015-03-09 17:45:42 +0000104 var item = this.liveItem(this._position);
105 if (item["further"] !== undefined) {
106 item["further"].bind(item).apply();
107 e.halt();
108 };
109 break;
110 case 13: // 'Enter'
111
112 // Click on prefix
113 if (this._prefix.active())
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000114 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000115
116 // Click on item
117 else
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000118 this.liveItem(this._position).onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000119 e.halt();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000120 break;
121 case 8: // 'Backspace'
Nils Diewald5975d702015-03-09 17:45:42 +0000122 this._prefix.backspace();
123 this.show();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000124 e.halt();
125 break;
126 default:
Nils Diewald5975d702015-03-09 17:45:42 +0000127 if (e.key !== undefined &&
128 e.key.length != 1)
Nils Diewald59c02fc2015-03-07 01:29:09 +0000129 return;
130
131 // Add prefix
Nils Diewald5975d702015-03-09 17:45:42 +0000132 this._prefix.add(e.key.toLowerCase());
133
134 if (!this.show()) {
135 this.prefix('').show();
136 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000137 };
138 },
139
Nils Diewald2fe12e12015-03-06 16:47:06 +0000140 // Initialize list
Nils Diewald5975d702015-03-09 17:45:42 +0000141 _init : function (itemClass, prefixClass, params) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000142 var that = this;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000143 this._itemClass = itemClass;
Nils Diewald5975d702015-03-09 17:45:42 +0000144
145 if (prefixClass !== undefined)
146 this._prefix = prefixClass.create();
147 else
148 this._prefix = KorAP.MenuPrefix.create();
149
150 var e = document.createElement("ul");
Nils Diewald59c02fc2015-03-07 01:29:09 +0000151 e.style.opacity = 0;
152 e.style.outline = 0;
153 e.setAttribute('tabindex', 0);
Nils Diewald5975d702015-03-09 17:45:42 +0000154 e.setAttribute('class', 'menu');
155 e.appendChild(this._prefix.element());
Nils Diewald86dad5b2015-01-28 15:09:07 +0000156
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000157 // This has to be cleaned up later on
158 e["menu"] = this;
159
Nils Diewald59c02fc2015-03-07 01:29:09 +0000160 // Arrow keys
161 e.addEventListener(
Nils Diewald5975d702015-03-09 17:45:42 +0000162 'keypress',
Nils Diewald59c02fc2015-03-07 01:29:09 +0000163 function (ev) {
Nils Diewald5975d702015-03-09 17:45:42 +0000164 that._keypress(ev)
Nils Diewald59c02fc2015-03-07 01:29:09 +0000165 },
166 false
167 );
168
169 // Mousewheel
170 e.addEventListener(
Nils Diewald5975d702015-03-09 17:45:42 +0000171 'wheel',
Nils Diewald59c02fc2015-03-07 01:29:09 +0000172 function (ev) {
173 that._mousewheel(ev)
Nils Diewald2fe12e12015-03-06 16:47:06 +0000174 },
175 false
176 );
177
Nils Diewald5975d702015-03-09 17:45:42 +0000178 this._element = e;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000179 this.active = false;
180 this._items = new Array();
181 var i;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000182
183 // Initialize item list based on parameters
Nils Diewald86dad5b2015-01-28 15:09:07 +0000184 for (i in params) {
185 var obj = itemClass.create(params[i]);
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000186
187 // This may become circular
188 obj["_menu"] = this;
189
Nils Diewald2fe12e12015-03-06 16:47:06 +0000190 this._items.push(obj);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000191 };
192 this._limit = KorAP.menuLimit;
193 this._position = 0; // position in the active list
194 this._active = -1; // active item in the item list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000195 this._reset();
196 return this;
197 },
198
Nils Diewald2fe12e12015-03-06 16:47:06 +0000199 /**
200 * Get the instantiated HTML element
201 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000202 element : function () {
203 return this._element;
204 },
205
Nils Diewald2fe12e12015-03-06 16:47:06 +0000206 /**
207 * Get the creator object for items
208 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000209 itemClass : function () {
210 return this._itemClass;
211 },
212
213 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000214 * Get and set numerical value for limit,
215 * i.e. the number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000216 */
217 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000218 if (arguments.length === 1) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000219 this._limit = limit;
Nils Diewald5975d702015-03-09 17:45:42 +0000220 return this;
221 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000222 return this._limit;
223 },
224
225 /**
226 * Upgrade this object to another object,
227 * while private data stays intact.
228 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000229 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000230 */
231 upgradeTo : function (props) {
232 for (var prop in props) {
233 this[prop] = props[prop];
234 };
235 return this;
236 },
237
Nils Diewald2fe12e12015-03-06 16:47:06 +0000238 // Reset chosen item and prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000239 _reset : function () {
240 this._offset = 0;
241 this._pos = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000242 this._prefix.value('');
Nils Diewald86dad5b2015-01-28 15:09:07 +0000243 },
244
245 /**
246 * Filter the list and make it visible
247 *
248 * @param {string} Prefix for filtering the list
249 */
Nils Diewald5975d702015-03-09 17:45:42 +0000250 show : function () {
Nils Diewalde8518f82015-03-18 22:41:49 +0000251
Nils Diewald86dad5b2015-01-28 15:09:07 +0000252 // Initialize the list
253 if (!this._initList())
Nils Diewald59c02fc2015-03-07 01:29:09 +0000254 return false;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000255
Nils Diewald2fe12e12015-03-06 16:47:06 +0000256 // show based on initial offset
Nils Diewald86dad5b2015-01-28 15:09:07 +0000257 this._showItems(0);
258
259 // Set the first element to active
Nils Diewald2fe12e12015-03-06 16:47:06 +0000260 // Todo: Or the last element chosen
Nils Diewald86dad5b2015-01-28 15:09:07 +0000261 this.liveItem(0).active(true);
Nils Diewalde8518f82015-03-18 22:41:49 +0000262 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000263
Nils Diewald86dad5b2015-01-28 15:09:07 +0000264 this._active = this._list[0];
Nils Diewald5975d702015-03-09 17:45:42 +0000265 this._position = 0;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000266 this._element.style.opacity = 1;
267
Nils Diewald86dad5b2015-01-28 15:09:07 +0000268 // Add classes for rolling menus
269 this._boundary(true);
Nils Diewald59c02fc2015-03-07 01:29:09 +0000270 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000271 },
272
Nils Diewald2fe12e12015-03-06 16:47:06 +0000273 hide : function () {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000274 this.active = false;
275 this.delete();
Nils Diewald2fe12e12015-03-06 16:47:06 +0000276 this._element.style.opacity = 0;
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000277 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000278 },
279
Nils Diewald2fe12e12015-03-06 16:47:06 +0000280 // Initialize the list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000281 _initList : function () {
282
Nils Diewald2fe12e12015-03-06 16:47:06 +0000283 // Create a new list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000284 if (this._list === undefined) {
285 this._list = [];
286 }
287 else if (this._list.length != 0) {
288 this._boundary(false);
289 this._list.length = 0;
290 };
291
292 // Offset is initially zero
293 this._offset = 0;
294
Nils Diewald2fe12e12015-03-06 16:47:06 +0000295 // There is no prefix set
Nils Diewald86dad5b2015-01-28 15:09:07 +0000296 if (this.prefix().length <= 0) {
Nils Diewald5975d702015-03-09 17:45:42 +0000297 var i = 0;
298 for (; i < this._items.length; i++)
Nils Diewald86dad5b2015-01-28 15:09:07 +0000299 this._list.push(i);
Nils Diewald5975d702015-03-09 17:45:42 +0000300 while (this._items[++i] !== undefined) {
301 this._items[i].lowlight();
302 console.log(this._item);
303 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000304 return true;
305 };
306
Nils Diewald2fe12e12015-03-06 16:47:06 +0000307 // There is a prefix set, so filter the list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000308 var pos;
309 var paddedPrefix = " " + this.prefix();
310
Nils Diewald2fe12e12015-03-06 16:47:06 +0000311 // Iterate over all items and choose preferred matching items
312 // i.e. the matching happens at the word start
Nils Diewald86dad5b2015-01-28 15:09:07 +0000313 for (pos = 0; pos < this._items.length; pos++) {
314 if ((this.item(pos).lcField().indexOf(paddedPrefix)) >= 0)
315 this._list.push(pos);
316 };
317
Nils Diewald2fe12e12015-03-06 16:47:06 +0000318 // The list is empty - so lower your expectations
319 // Iterate over all items and choose matching items
320 // i.e. the matching happens anywhere in the word
Nils Diewald86dad5b2015-01-28 15:09:07 +0000321 if (this._list.length == 0) {
322 for (pos = 0; pos < this._items.length; pos++) {
323 if ((this.item(pos).lcField().indexOf(this.prefix())) >= 0)
324 this._list.push(pos);
325 };
326 };
327
Nils Diewald2fe12e12015-03-06 16:47:06 +0000328 // Filter was successful - yeah!
Nils Diewald86dad5b2015-01-28 15:09:07 +0000329 return this._list.length > 0 ? true : false;
330 },
331
332 // Set boundary for viewport
333 _boundary : function (bool) {
334 this.item(this._list[0]).noMore(bool);
335 this.item(this._list[this._list.length - 1]).noMore(bool);
336 },
337
338 /**
339 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000340 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000341 */
Nils Diewald5975d702015-03-09 17:45:42 +0000342 prefix : function (pref) {
343 if (arguments.length === 1) {
344 this._prefix.value(pref);
345 return this;
346 };
347 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000348 },
349
Nils Diewald2fe12e12015-03-06 16:47:06 +0000350 // Append Items that should be shown
Nils Diewald86dad5b2015-01-28 15:09:07 +0000351 _showItems : function (offset) {
352 this.delete();
353
354 // Use list
355 var shown = 0;
356 var i;
357 for (i in this._list) {
358
359 // Don't show - it's before offset
360 if (shown++ < offset)
361 continue;
362
363 this._append(this._list[i]);
364
365 if (shown >= (this.limit() + this._offset))
366 break;
367 };
368 },
369
370 /**
371 * Delete all visible items from the menu element
372 */
373 delete : function () {
374 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000375
Nils Diewald5975d702015-03-09 17:45:42 +0000376 /*
Nils Diewald2fe12e12015-03-06 16:47:06 +0000377 // Iterate over all visible items
Nils Diewald86dad5b2015-01-28 15:09:07 +0000378 for (var i = 0; i <= this.limit(); i++) {
379
Nils Diewald5975d702015-03-09 17:45:42 +0000380 // there is a visible element
381 // unhighlight!
Nils Diewald59c02fc2015-03-07 01:29:09 +0000382 if (child = this.shownItem(i)) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000383 child.lowlight();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000384 child.active(false);
385 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000386 };
Nils Diewald5975d702015-03-09 17:45:42 +0000387 */
388
389 for (var i in this._list) {
390 var item = this._items[this._list[i]];
391 item.lowlight();
392 item.active(false);
393 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000394
Nils Diewald2fe12e12015-03-06 16:47:06 +0000395 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000396 var children = this._element.childNodes;
397 for (var i = children.length - 1; i >= 1; i--) {
398 this._element.removeChild(
399 children[i]
400 );
401 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000402 },
403
404
405 // Append item to the shown list based on index
406 _append : function (i) {
407 var item = this.item(i);
408
409 // Highlight based on prefix
410 if (this.prefix().length > 0)
411 item.highlight(this.prefix());
412
413 // Append element
414 this.element().appendChild(item.element());
415 },
416
417
Nils Diewald2fe12e12015-03-06 16:47:06 +0000418 // Prepend item to the shown list based on index
419 _prepend : function (i) {
420 var item = this.item(i);
421
422 // Highlight based on prefix
423 if (this.prefix().length > 0)
424 item.highlight(this.prefix());
425
426 var e = this.element();
427 // Append element
428 e.insertBefore(
429 item.element(),
Nils Diewald5975d702015-03-09 17:45:42 +0000430 e.children[1]
Nils Diewald2fe12e12015-03-06 16:47:06 +0000431 );
432 },
433
434
435 /**
436 * Get a specific item from the complete list
437 *
438 * @param {number} index of the list item
439 */
440 item : function (index) {
441 return this._items[index]
442 },
443
444
Nils Diewald86dad5b2015-01-28 15:09:07 +0000445 /**
446 * Get a specific item from the filtered list
447 *
448 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000449 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000450 */
451 liveItem : function (index) {
452 if (this._list === undefined)
453 if (!this._initList())
454 return;
455
456 return this._items[this._list[index]];
457 },
458
Nils Diewald86dad5b2015-01-28 15:09:07 +0000459
460 /**
461 * Get a specific item from the visible list
462 *
463 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000464 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000465 */
466 shownItem : function (index) {
467 if (index >= this.limit())
468 return;
469 return this.liveItem(this._offset + index);
470 },
471
472
Nils Diewald2fe12e12015-03-06 16:47:06 +0000473 /**
474 * Get the length of the full list
475 */
476 length : function () {
477 return this._items.length;
478 },
479
480
481 /**
482 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000483 */
484 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000485
Nils Diewald86dad5b2015-01-28 15:09:07 +0000486 // No active element set
Nils Diewald5975d702015-03-09 17:45:42 +0000487 if (this._position === -1)
Nils Diewald86dad5b2015-01-28 15:09:07 +0000488 return;
489
Nils Diewald5975d702015-03-09 17:45:42 +0000490 var newItem;
491
Nils Diewald86dad5b2015-01-28 15:09:07 +0000492 // Set new live item
Nils Diewalde8518f82015-03-18 22:41:49 +0000493 if (!this._prefix.active()) {
494 var oldItem = this.liveItem(this._position);
495 oldItem.active(false);
496 };
497
498 this._position++;
499
Nils Diewald5975d702015-03-09 17:45:42 +0000500 newItem = this.liveItem(this._position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000501
Nils Diewald5975d702015-03-09 17:45:42 +0000502 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000503 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000504
505 // Activate prefix
506 var prefix = this._prefix;
507
508 // Mark prefix
509 if (prefix.isSet() && !prefix.active()) {
510 this._position--;
511 prefix.active(true);
512 return;
513 }
514 else {
515 this._offset = 0;
516 this._position = 0;
517 newItem = this.liveItem(0);
518 this._showItems(0);
519 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000520 }
521
522 // The next element is outside the view - roll down
Nils Diewald2fe12e12015-03-06 16:47:06 +0000523 else if (this._position >= (this.limit() + this._offset)) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000524 this._removeFirst();
525 this._offset++;
526 this._append(this._list[this._position]);
527 };
Nils Diewald5975d702015-03-09 17:45:42 +0000528
529 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000530 newItem.active(true);
531 },
532
Nils Diewalde8518f82015-03-18 22:41:49 +0000533 /*
534 * Page down to the first item on the next page
535 */
536 /*
537 nextPage : function () {
538
539 // Prefix is active
540 if (this._prefix.active()) {
541 this._prefix.active(false);
542 }
543
544 // Last item is chosen
545 else if (this._position >= this.limit() + this._offset) {
546
547 this._position = this.limit() + this._offset - 1;
548 newItem = this.liveItem(this._position);
549 var oldItem = this.liveItem(this._position--);
550 oldItem.active(false);
551 }
552
553 // Last item of page is chosen
554 else if (0) {
555
556 // Jump to last item
557 else {
558 var oldItem = this.liveItem(this._position);
559 oldItem.active(false);
560
561 this._position = this.limit() + this._offset - 1;
562 newItem = this.liveItem(this._position);
563 };
564
565 newItem.active(true);
566 },
567 */
568
Nils Diewald86dad5b2015-01-28 15:09:07 +0000569
570 /*
571 * Make the previous item in the menu active
572 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000573 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +0000574
Nils Diewald2fe12e12015-03-06 16:47:06 +0000575 // No active element set
Nils Diewalde8518f82015-03-18 22:41:49 +0000576 if (this._position === -1) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000577 return;
Nils Diewalde8518f82015-03-18 22:41:49 +0000578 // TODO: Choose last item
579 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000580
Nils Diewald5975d702015-03-09 17:45:42 +0000581 var newItem;
582
Nils Diewald86dad5b2015-01-28 15:09:07 +0000583 // Set new live item
Nils Diewald2d210752015-03-09 19:01:15 +0000584 if (!this._prefix.active()) {
585 var oldItem = this.liveItem(this._position--);
586 oldItem.active(false);
587 };
588
Nils Diewald5975d702015-03-09 17:45:42 +0000589 newItem = this.liveItem(this._position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000590
591 // The previous element is undefined - roll to bottom
592 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000593
594 // Activate prefix
595 var prefix = this._prefix;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000596 this._offset = this.liveLength() - this.limit();
Nils Diewald2d210752015-03-09 19:01:15 +0000597
598 // Normalize offset
599 this._offset = this._offset < 0 ? 0 : this._offset;
600
Nils Diewald2fe12e12015-03-06 16:47:06 +0000601 this._position = this.liveLength() - 1;
Nils Diewald5975d702015-03-09 17:45:42 +0000602
603 if (prefix.isSet() && !prefix.active()) {
604 this._position++;
605 prefix.active(true);
606 return;
607 }
608 else {
609 newItem = this.liveItem(this._position);
610 this._showItems(this._offset);
611 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000612 }
613
614 // The previous element is outside the view - roll up
615 else if (this._position < this._offset) {
616 this._removeLast();
617 this._offset--;
618 this._prepend(this._list[this._position]);
619 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000620
Nils Diewald5975d702015-03-09 17:45:42 +0000621 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000622 newItem.active(true);
623 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000624
625
Nils Diewald5975d702015-03-09 17:45:42 +0000626 // Length of the filtered list
627 liveLength : function () {
628 if (this._list === undefined)
629 this._initList();
630 return this._list.length;
631 },
632
633
Nils Diewald2fe12e12015-03-06 16:47:06 +0000634 // Remove the HTML node from the first item
Nils Diewald86dad5b2015-01-28 15:09:07 +0000635 _removeFirst : function () {
636 this.item(this._list[this._offset]).lowlight();
Nils Diewald5975d702015-03-09 17:45:42 +0000637 this._element.removeChild(this._element.children[1]);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000638 },
639
Nils Diewald2fe12e12015-03-06 16:47:06 +0000640
641 // Remove the HTML node from the last item
Nils Diewald86dad5b2015-01-28 15:09:07 +0000642 _removeLast : function () {
Nils Diewald2fe12e12015-03-06 16:47:06 +0000643 this.item(this._list[this._offset + this.limit() - 1]).lowlight();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000644 this._element.removeChild(this._element.lastChild);
Nils Diewald2fe12e12015-03-06 16:47:06 +0000645 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000646 };
647
648
Nils Diewaldfda29d92015-01-22 17:28:01 +0000649 /**
650 * Item in the Dropdown menu
651 */
652 KorAP.MenuItem = {
653
654 /**
655 * Create a new MenuItem object.
656 *
657 * @constructor
658 * @this {MenuItem}
659 * @param {Array.<string>} An array object of name, action and
660 * optionally a description
661 */
662 create : function (params) {
663 return Object.create(KorAP.MenuItem)._init(params);
664 },
665
666 /**
667 * Upgrade this object to another object,
668 * while private data stays intact.
669 *
670 * @param {Object] An object with properties.
671 */
672 upgradeTo : function (props) {
673 for (var prop in props) {
674 this[prop] = props[prop];
675 };
676 return this;
677 },
678
Nils Diewald5975d702015-03-09 17:45:42 +0000679
Nils Diewaldfda29d92015-01-22 17:28:01 +0000680 content : function (content) {
681 if (arguments.length === 1)
682 this._content = document.createTextNode(content);
683 return this._content;
684 },
685
686 lcField : function () {
687 return this._lcField;
688 },
689
690 action : function (action) {
691 if (arguments.length === 1)
692 this._action = action;
693 return this._action;
694 },
695
696 /**
697 * Check or set if the item is active
698 *
699 * @param {boolean|null} State of activity
700 */
701 active : function (bool) {
702 var cl = this.element().classList;
703 if (bool === undefined)
704 return cl.contains("active");
705 else if (bool)
706 cl.add("active");
707 else
708 cl.remove("active");
709 },
710
711 /**
712 * Check or set if the item is
713 * at the boundary of the menu
714 * list
715 *
716 * @param {boolean|null} State of activity
717 */
718 noMore : function (bool) {
719 var cl = this.element().classList;
720 if (bool === undefined)
721 return cl.contains("no-more");
722 else if (bool)
723 cl.add("no-more");
724 else
725 cl.remove("no-more");
726 },
727
728 /**
729 * Get the document element of the menu item
730 */
731 element : function () {
732 // already defined
733 if (this._element !== undefined)
734 return this._element;
735
736 // Create list item
737 var li = document.createElement("li");
738
739 // Connect action
Nils Diewald5975d702015-03-09 17:45:42 +0000740 if (this.onclick !== undefined)
741 li["onclick"] = this.onclick.bind(this);
Nils Diewaldfda29d92015-01-22 17:28:01 +0000742
743 // Append template
744 li.appendChild(this.content());
745
746 return this._element = li;
747 },
748
749 /**
750 * Highlight parts of the item
751 *
752 * @param {string} Prefix string for highlights
753 */
754 highlight : function (prefix) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000755 var children = this.element().childNodes;
756 for (var i = children.length -1; i >= 0; i--) {
757 this._highlight(children[i], prefix);
758 };
Nils Diewaldfda29d92015-01-22 17:28:01 +0000759 },
760
761 // Highlight a certain substring of the menu item
762 _highlight : function (elem, prefix) {
763
764 if (elem.nodeType === 3) {
765
766 var text = elem.nodeValue;
767 var textlc = text.toLowerCase();
768 var pos = textlc.indexOf(prefix);
769 if (pos >= 0) {
770
771 // First element
772 if (pos > 0) {
773 elem.parentNode.insertBefore(
774 document.createTextNode(text.substr(0, pos)),
775 elem
776 );
777 };
778
779 // Second element
780 var hl = document.createElement("mark");
781 hl.appendChild(
782 document.createTextNode(text.substr(pos, prefix.length))
783 );
784 elem.parentNode.insertBefore(hl, elem);
785
786 // Third element
787 var third = text.substr(pos + prefix.length);
788 if (third.length > 0) {
789 var thirdE = document.createTextNode(third);
790 elem.parentNode.insertBefore(
791 thirdE,
792 elem
793 );
794 this._highlight(thirdE, prefix);
795 };
796
797 var p = elem.parentNode;
798 p.removeChild(elem);
799 };
800 }
801 else {
802 var children = elem.childNodes;
803 for (var i = children.length -1; i >= 0; i--) {
804 this._highlight(children[i], prefix);
805 };
806 };
807 },
808
809
810 /**
811 * Remove highlight of the menu item
812 */
813 lowlight : function () {
814 var e = this.element();
815
816 var marks = e.getElementsByTagName("mark");
817 for (var i = marks.length - 1; i >= 0; i--) {
818 // Create text node clone
819 var x = document.createTextNode(
820 marks[i].firstChild.nodeValue
821 );
822
823 // Replace with content
824 marks[i].parentNode.replaceChild(
825 x,
826 marks[i]
827 );
828 };
829
830 // Remove consecutive textnodes
831 e.normalize();
832 },
833
834 // Initialize menu item
835 _init : function (params) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000836
Nils Diewaldfda29d92015-01-22 17:28:01 +0000837 if (params[0] === undefined)
838 throw new Error("Missing parameters");
839
840 this.content(params[0]);
841
842 if (params.length === 2)
843 this._action = params[1];
844
845 this._lcField = ' ' + this.content().textContent.toLowerCase();
846
847 return this;
848 },
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000849
850 /**
851 * Return menu list.
852 */
853 menu : function () {
854 return this._menu;
855 }
Nils Diewaldfda29d92015-01-22 17:28:01 +0000856 };
857
Nils Diewald5975d702015-03-09 17:45:42 +0000858 KorAP.MenuPrefix = {
859 create : function (params) {
860 return Object.create(KorAP.MenuPrefix)._init();
861 },
862 _init : function () {
863 this._string = '';
864
865 // Add prefix span
866 this._element = document.createElement('span');
867 this._element.classList.add('pref');
Nils Diewalde8518f82015-03-18 22:41:49 +0000868 // Connect action
869 if (this.onclick !== undefined)
870 this._element["onclick"] = this.onclick.bind(this);
871
Nils Diewald5975d702015-03-09 17:45:42 +0000872 return this;
873 },
874 _update : function () {
875 this._element.innerHTML
876 = this._string;
877 },
878
879 /**
880 * Upgrade this object to another object,
881 * while private data stays intact.
882 *
883 * @param {Object} An object with properties.
884 */
885 upgradeTo : function (props) {
886 for (var prop in props) {
887 this[prop] = props[prop];
888 };
889 return this;
890 },
891
892 active : function (bool) {
893 var cl = this.element().classList;
894 if (bool === undefined)
895 return cl.contains("active");
896 else if (bool)
897 cl.add("active");
898 else
899 cl.remove("active");
900 },
901 element : function () {
902 return this._element;
903 },
904 isSet : function () {
905 return this._string.length > 0 ?
906 true : false;
907 },
908 value : function (string) {
909 if (arguments.length === 1) {
910 this._string = string;
911 this._update();
912 };
913 return this._string;
914 },
915 add : function (string) {
916 this._string += string;
917 this._update();
918 },
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000919 onclick : function (e) {},
Nils Diewald5975d702015-03-09 17:45:42 +0000920 backspace : function () {
921 if (this._string.length > 1) {
922 this._string = this._string.substring(
923 0, this._string.length - 1
924 );
925 }
926 else {
927 this._string = '';
928 };
929
930 this._update();
931 }
932 };
933
Nils Diewald59c02fc2015-03-07 01:29:09 +0000934 function _codeFromEvent (e) {
Nils Diewald5975d702015-03-09 17:45:42 +0000935 if (e.charCode && (e.keyCode == 0))
Nils Diewald59c02fc2015-03-07 01:29:09 +0000936 return e.charCode
937 return e.keyCode;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000938 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000939
Nils Diewaldfda29d92015-01-22 17:28:01 +0000940}(this.KorAP));
Nils Diewald5975d702015-03-09 17:45:42 +0000941
942/**
943 * MenuItems may define:
944 *
945 * onclick: action happen on click and enter.
946 * further: action happen on right arrow
947 */