blob: 78f9ea7199752a927f17e06ed3dc7d2abd243660 [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 Diewald2488d052015-04-09 21:46:02 +00009/*
10 * TODO: space is not a valid prefix!
11 */
Nils Diewaldfda29d92015-01-22 17:28:01 +000012(function (KorAP) {
13 "use strict";
14
Nils Diewald59c02fc2015-03-07 01:29:09 +000015 // Don't let events bubble up
Nils Diewaldd2b57372015-03-10 20:09:48 +000016 if (Event.halt === undefined) {
17 // Don't let events bubble up
18 Event.prototype.halt = function () {
19 this.stopPropagation();
20 this.preventDefault();
21 };
Nils Diewald59c02fc2015-03-07 01:29:09 +000022 };
23
Nils Diewald86dad5b2015-01-28 15:09:07 +000024 // Default maximum number of menu items
25 KorAP.menuLimit = 8;
26
27 /**
28 * List of items for drop down menu (complete).
29 * Only a sublist of the menu is filtered (live).
30 * Only a sublist of the filtered menu is visible (shown).
31 */
32 KorAP.Menu = {
33 /**
34 * Create new Menu based on the action prefix
35 * and a list of menu items.
36 *
37 * @this {Menu}
38 * @constructor
39 * @param {string} Context prefix
40 * @param {Array.<Array.<string>>} List of menu items
41 */
42 create : function (params) {
43 return Object.create(KorAP.Menu)._init(params);
44 },
45
Nils Diewald6e43ffd2015-03-25 18:55:39 +000046 /**
47 * Destroy this menu
48 * (in case you don't trust the
49 * mark and sweep GC)!
50 */
51 destroy : function () {
52 if (this._element != undefined)
53 delete this._element["menu"];
54
55 for (var i = 0; i < this._items.length; i++) {
56 delete this._items[i]["_menu"];
57 };
Nils Diewald5c5a7472015-04-02 22:13:38 +000058 delete this._prefix['_menu'];
Nils Diewald6e43ffd2015-03-25 18:55:39 +000059 },
60
Nils Diewald2fe12e12015-03-06 16:47:06 +000061 focus : function () {
62 this._element.focus();
63 },
64
Nils Diewald59c02fc2015-03-07 01:29:09 +000065 // mouse wheel treatment
66 _mousewheel : function (e) {
67 var delta = 0;
Nils Diewald5975d702015-03-09 17:45:42 +000068
69 delta = e.deltaY / 120;
70 if (delta > 0)
Nils Diewald59c02fc2015-03-07 01:29:09 +000071 this.next();
Nils Diewald5975d702015-03-09 17:45:42 +000072 else if (delta < 0)
Nils Diewald59c02fc2015-03-07 01:29:09 +000073 this.prev();
Nils Diewald59c02fc2015-03-07 01:29:09 +000074 e.halt();
75 },
76
77 // Arrow key and prefix treatment
Nils Diewald5975d702015-03-09 17:45:42 +000078 _keypress : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +000079 var code = _codeFromEvent(e);
80
Nils Diewald59c02fc2015-03-07 01:29:09 +000081 switch (code) {
82 case 27: // 'Esc'
83 e.halt();
84 this.hide();
85 break;
Nils Diewald5975d702015-03-09 17:45:42 +000086
Nils Diewald59c02fc2015-03-07 01:29:09 +000087 case 38: // 'Up'
88 e.halt();
89 this.prev();
90 break;
Nils Diewald5975d702015-03-09 17:45:42 +000091 case 33: // 'Page up'
Nils Diewald59c02fc2015-03-07 01:29:09 +000092 e.halt();
Nils Diewald5975d702015-03-09 17:45:42 +000093 this.prev();
94 break;
95 case 40: // 'Down'
96 e.halt();
97 this.next();
98 break;
99 case 34: // 'Page down'
100 e.halt();
101 this.next();
102 break;
103 case 39: // 'Right'
Nils Diewalde8518f82015-03-18 22:41:49 +0000104 if (this._prefix.active())
105 break;
106
Nils Diewald5975d702015-03-09 17:45:42 +0000107 var item = this.liveItem(this._position);
108 if (item["further"] !== undefined) {
109 item["further"].bind(item).apply();
110 e.halt();
111 };
112 break;
113 case 13: // 'Enter'
114
115 // Click on prefix
116 if (this._prefix.active())
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000117 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000118
119 // Click on item
120 else
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000121 this.liveItem(this._position).onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000122 e.halt();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000123 break;
124 case 8: // 'Backspace'
Nils Diewald5975d702015-03-09 17:45:42 +0000125 this._prefix.backspace();
126 this.show();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000127 e.halt();
128 break;
129 default:
Nils Diewald5975d702015-03-09 17:45:42 +0000130 if (e.key !== undefined &&
131 e.key.length != 1)
Nils Diewald59c02fc2015-03-07 01:29:09 +0000132 return;
133
134 // Add prefix
Nils Diewald5975d702015-03-09 17:45:42 +0000135 this._prefix.add(e.key.toLowerCase());
136
137 if (!this.show()) {
138 this.prefix('').show();
139 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000140 };
141 },
142
Nils Diewald2fe12e12015-03-06 16:47:06 +0000143 // Initialize list
Nils Diewald5975d702015-03-09 17:45:42 +0000144 _init : function (itemClass, prefixClass, params) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000145 var that = this;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000146 this._itemClass = itemClass;
Nils Diewald5975d702015-03-09 17:45:42 +0000147
148 if (prefixClass !== undefined)
149 this._prefix = prefixClass.create();
150 else
151 this._prefix = KorAP.MenuPrefix.create();
152
Nils Diewald5c5a7472015-04-02 22:13:38 +0000153 this._prefix._menu = this;
154
Nils Diewald5975d702015-03-09 17:45:42 +0000155 var e = document.createElement("ul");
Nils Diewald59c02fc2015-03-07 01:29:09 +0000156 e.style.opacity = 0;
157 e.style.outline = 0;
158 e.setAttribute('tabindex', 0);
Nils Diewald5c5a7472015-04-02 22:13:38 +0000159 e.classList.add('menu');
Nils Diewald58141332015-04-07 16:18:45 +0000160 e.classList.add('roll');
Nils Diewald5975d702015-03-09 17:45:42 +0000161 e.appendChild(this._prefix.element());
Nils Diewald86dad5b2015-01-28 15:09:07 +0000162
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000163 // This has to be cleaned up later on
164 e["menu"] = this;
165
Nils Diewald59c02fc2015-03-07 01:29:09 +0000166 // Arrow keys
167 e.addEventListener(
Nils Diewald5975d702015-03-09 17:45:42 +0000168 'keypress',
Nils Diewald59c02fc2015-03-07 01:29:09 +0000169 function (ev) {
Nils Diewald5975d702015-03-09 17:45:42 +0000170 that._keypress(ev)
Nils Diewald59c02fc2015-03-07 01:29:09 +0000171 },
172 false
173 );
174
175 // Mousewheel
176 e.addEventListener(
Nils Diewald5975d702015-03-09 17:45:42 +0000177 'wheel',
Nils Diewald59c02fc2015-03-07 01:29:09 +0000178 function (ev) {
179 that._mousewheel(ev)
Nils Diewald2fe12e12015-03-06 16:47:06 +0000180 },
181 false
182 );
183
Nils Diewald5975d702015-03-09 17:45:42 +0000184 this._element = e;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000185 this.active = false;
186 this._items = new Array();
187 var i;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000188
189 // Initialize item list based on parameters
Nils Diewald86dad5b2015-01-28 15:09:07 +0000190 for (i in params) {
191 var obj = itemClass.create(params[i]);
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000192
193 // This may become circular
194 obj["_menu"] = this;
195
Nils Diewald2fe12e12015-03-06 16:47:06 +0000196 this._items.push(obj);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000197 };
198 this._limit = KorAP.menuLimit;
199 this._position = 0; // position in the active list
200 this._active = -1; // active item in the item list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000201 this._reset();
202 return this;
203 },
204
Nils Diewald2fe12e12015-03-06 16:47:06 +0000205 /**
206 * Get the instantiated HTML element
207 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000208 element : function () {
209 return this._element;
210 },
211
Nils Diewald2fe12e12015-03-06 16:47:06 +0000212 /**
213 * Get the creator object for items
214 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000215 itemClass : function () {
216 return this._itemClass;
217 },
218
219 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000220 * Get and set numerical value for limit,
221 * i.e. the number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000222 */
223 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000224 if (arguments.length === 1) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000225 this._limit = limit;
Nils Diewald5975d702015-03-09 17:45:42 +0000226 return this;
227 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000228 return this._limit;
229 },
230
231 /**
232 * Upgrade this object to another object,
233 * while private data stays intact.
234 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000235 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000236 */
237 upgradeTo : function (props) {
238 for (var prop in props) {
239 this[prop] = props[prop];
240 };
241 return this;
242 },
243
Nils Diewald2fe12e12015-03-06 16:47:06 +0000244 // Reset chosen item and prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000245 _reset : function () {
246 this._offset = 0;
247 this._pos = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000248 this._prefix.value('');
Nils Diewald86dad5b2015-01-28 15:09:07 +0000249 },
250
251 /**
252 * Filter the list and make it visible
253 *
254 * @param {string} Prefix for filtering the list
255 */
Nils Diewald5975d702015-03-09 17:45:42 +0000256 show : function () {
Nils Diewalde8518f82015-03-18 22:41:49 +0000257
Nils Diewald86dad5b2015-01-28 15:09:07 +0000258 // Initialize the list
259 if (!this._initList())
Nils Diewald59c02fc2015-03-07 01:29:09 +0000260 return false;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000261
Nils Diewald2fe12e12015-03-06 16:47:06 +0000262 // show based on initial offset
Nils Diewald86dad5b2015-01-28 15:09:07 +0000263 this._showItems(0);
264
265 // Set the first element to active
Nils Diewald2fe12e12015-03-06 16:47:06 +0000266 // Todo: Or the last element chosen
Nils Diewald86dad5b2015-01-28 15:09:07 +0000267 this.liveItem(0).active(true);
Nils Diewalde8518f82015-03-18 22:41:49 +0000268 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000269
Nils Diewald86dad5b2015-01-28 15:09:07 +0000270 this._active = this._list[0];
Nils Diewald5975d702015-03-09 17:45:42 +0000271 this._position = 0;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000272 this._element.style.opacity = 1;
273
Nils Diewald86dad5b2015-01-28 15:09:07 +0000274 // Add classes for rolling menus
275 this._boundary(true);
Nils Diewald59c02fc2015-03-07 01:29:09 +0000276 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000277 },
278
Nils Diewald2fe12e12015-03-06 16:47:06 +0000279 hide : function () {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000280 this.active = false;
281 this.delete();
Nils Diewald2fe12e12015-03-06 16:47:06 +0000282 this._element.style.opacity = 0;
Nils Diewald5c5a7472015-04-02 22:13:38 +0000283 this.onHide();
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000284 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000285 },
286
Nils Diewald5c5a7472015-04-02 22:13:38 +0000287 // To be override
288 onHide : function () {},
289
Nils Diewald2fe12e12015-03-06 16:47:06 +0000290 // Initialize the list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000291 _initList : function () {
292
Nils Diewald2fe12e12015-03-06 16:47:06 +0000293 // Create a new list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000294 if (this._list === undefined) {
295 this._list = [];
296 }
297 else if (this._list.length != 0) {
298 this._boundary(false);
299 this._list.length = 0;
300 };
301
302 // Offset is initially zero
303 this._offset = 0;
304
Nils Diewald2fe12e12015-03-06 16:47:06 +0000305 // There is no prefix set
Nils Diewald86dad5b2015-01-28 15:09:07 +0000306 if (this.prefix().length <= 0) {
Nils Diewald5975d702015-03-09 17:45:42 +0000307 var i = 0;
308 for (; i < this._items.length; i++)
Nils Diewald86dad5b2015-01-28 15:09:07 +0000309 this._list.push(i);
Nils Diewald5975d702015-03-09 17:45:42 +0000310 while (this._items[++i] !== undefined) {
311 this._items[i].lowlight();
Nils Diewald2488d052015-04-09 21:46:02 +0000312 // console.log(this._item);
Nils Diewald5975d702015-03-09 17:45:42 +0000313 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000314 return true;
315 };
316
Nils Diewald2fe12e12015-03-06 16:47:06 +0000317 // There is a prefix set, so filter the list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000318 var pos;
319 var paddedPrefix = " " + this.prefix();
320
Nils Diewald2fe12e12015-03-06 16:47:06 +0000321 // Iterate over all items and choose preferred matching items
322 // i.e. the matching happens at the word start
Nils Diewald86dad5b2015-01-28 15:09:07 +0000323 for (pos = 0; pos < this._items.length; pos++) {
324 if ((this.item(pos).lcField().indexOf(paddedPrefix)) >= 0)
325 this._list.push(pos);
326 };
327
Nils Diewald2fe12e12015-03-06 16:47:06 +0000328 // The list is empty - so lower your expectations
329 // Iterate over all items and choose matching items
330 // i.e. the matching happens anywhere in the word
Nils Diewald86dad5b2015-01-28 15:09:07 +0000331 if (this._list.length == 0) {
332 for (pos = 0; pos < this._items.length; pos++) {
333 if ((this.item(pos).lcField().indexOf(this.prefix())) >= 0)
334 this._list.push(pos);
335 };
336 };
337
Nils Diewald2fe12e12015-03-06 16:47:06 +0000338 // Filter was successful - yeah!
Nils Diewald86dad5b2015-01-28 15:09:07 +0000339 return this._list.length > 0 ? true : false;
340 },
341
342 // Set boundary for viewport
343 _boundary : function (bool) {
344 this.item(this._list[0]).noMore(bool);
345 this.item(this._list[this._list.length - 1]).noMore(bool);
346 },
347
348 /**
349 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000350 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000351 */
Nils Diewald5975d702015-03-09 17:45:42 +0000352 prefix : function (pref) {
353 if (arguments.length === 1) {
354 this._prefix.value(pref);
355 return this;
356 };
357 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000358 },
359
Nils Diewald2fe12e12015-03-06 16:47:06 +0000360 // Append Items that should be shown
Nils Diewald86dad5b2015-01-28 15:09:07 +0000361 _showItems : function (offset) {
362 this.delete();
363
364 // Use list
365 var shown = 0;
366 var i;
367 for (i in this._list) {
368
369 // Don't show - it's before offset
370 if (shown++ < offset)
371 continue;
372
373 this._append(this._list[i]);
374
375 if (shown >= (this.limit() + this._offset))
376 break;
377 };
378 },
379
380 /**
381 * Delete all visible items from the menu element
382 */
383 delete : function () {
384 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000385
Nils Diewald5975d702015-03-09 17:45:42 +0000386 /*
Nils Diewald2fe12e12015-03-06 16:47:06 +0000387 // Iterate over all visible items
Nils Diewald86dad5b2015-01-28 15:09:07 +0000388 for (var i = 0; i <= this.limit(); i++) {
389
Nils Diewald5975d702015-03-09 17:45:42 +0000390 // there is a visible element
391 // unhighlight!
Nils Diewald59c02fc2015-03-07 01:29:09 +0000392 if (child = this.shownItem(i)) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000393 child.lowlight();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000394 child.active(false);
395 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000396 };
Nils Diewald5975d702015-03-09 17:45:42 +0000397 */
398
399 for (var i in this._list) {
400 var item = this._items[this._list[i]];
401 item.lowlight();
402 item.active(false);
403 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000404
Nils Diewald2fe12e12015-03-06 16:47:06 +0000405 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000406 var children = this._element.childNodes;
407 for (var i = children.length - 1; i >= 1; i--) {
408 this._element.removeChild(
409 children[i]
410 );
411 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000412 },
413
414
415 // Append item to the shown list based on index
416 _append : function (i) {
417 var item = this.item(i);
418
419 // Highlight based on prefix
420 if (this.prefix().length > 0)
421 item.highlight(this.prefix());
422
423 // Append element
424 this.element().appendChild(item.element());
425 },
426
427
Nils Diewald2fe12e12015-03-06 16:47:06 +0000428 // Prepend item to the shown list based on index
429 _prepend : function (i) {
430 var item = this.item(i);
431
432 // Highlight based on prefix
433 if (this.prefix().length > 0)
434 item.highlight(this.prefix());
435
436 var e = this.element();
437 // Append element
438 e.insertBefore(
439 item.element(),
Nils Diewald5975d702015-03-09 17:45:42 +0000440 e.children[1]
Nils Diewald2fe12e12015-03-06 16:47:06 +0000441 );
442 },
443
444
445 /**
446 * Get a specific item from the complete list
447 *
448 * @param {number} index of the list item
449 */
450 item : function (index) {
451 return this._items[index]
452 },
453
454
Nils Diewald86dad5b2015-01-28 15:09:07 +0000455 /**
456 * Get a specific item from the filtered list
457 *
458 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000459 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000460 */
461 liveItem : function (index) {
462 if (this._list === undefined)
463 if (!this._initList())
464 return;
465
466 return this._items[this._list[index]];
467 },
468
Nils Diewald86dad5b2015-01-28 15:09:07 +0000469
470 /**
471 * Get a specific item from the visible list
472 *
473 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000474 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000475 */
476 shownItem : function (index) {
477 if (index >= this.limit())
478 return;
479 return this.liveItem(this._offset + index);
480 },
481
482
Nils Diewald2fe12e12015-03-06 16:47:06 +0000483 /**
484 * Get the length of the full list
485 */
486 length : function () {
487 return this._items.length;
488 },
489
490
491 /**
492 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000493 */
494 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000495
Nils Diewald86dad5b2015-01-28 15:09:07 +0000496 // No active element set
Nils Diewald5975d702015-03-09 17:45:42 +0000497 if (this._position === -1)
Nils Diewald86dad5b2015-01-28 15:09:07 +0000498 return;
499
Nils Diewald5975d702015-03-09 17:45:42 +0000500 var newItem;
501
Nils Diewald86dad5b2015-01-28 15:09:07 +0000502 // Set new live item
Nils Diewalde8518f82015-03-18 22:41:49 +0000503 if (!this._prefix.active()) {
504 var oldItem = this.liveItem(this._position);
505 oldItem.active(false);
506 };
507
508 this._position++;
509
Nils Diewald5975d702015-03-09 17:45:42 +0000510 newItem = this.liveItem(this._position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000511
Nils Diewald5975d702015-03-09 17:45:42 +0000512 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000513 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000514
515 // Activate prefix
516 var prefix = this._prefix;
517
518 // Mark prefix
519 if (prefix.isSet() && !prefix.active()) {
520 this._position--;
521 prefix.active(true);
522 return;
523 }
524 else {
525 this._offset = 0;
526 this._position = 0;
527 newItem = this.liveItem(0);
528 this._showItems(0);
529 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000530 }
531
532 // The next element is outside the view - roll down
Nils Diewald2fe12e12015-03-06 16:47:06 +0000533 else if (this._position >= (this.limit() + this._offset)) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000534 this._removeFirst();
535 this._offset++;
536 this._append(this._list[this._position]);
537 };
Nils Diewald5975d702015-03-09 17:45:42 +0000538
539 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000540 newItem.active(true);
541 },
542
Nils Diewalde8518f82015-03-18 22:41:49 +0000543 /*
544 * Page down to the first item on the next page
545 */
546 /*
547 nextPage : function () {
548
549 // Prefix is active
550 if (this._prefix.active()) {
551 this._prefix.active(false);
552 }
553
554 // Last item is chosen
555 else if (this._position >= this.limit() + this._offset) {
556
557 this._position = this.limit() + this._offset - 1;
558 newItem = this.liveItem(this._position);
559 var oldItem = this.liveItem(this._position--);
560 oldItem.active(false);
561 }
562
563 // Last item of page is chosen
564 else if (0) {
565
566 // Jump to last item
567 else {
568 var oldItem = this.liveItem(this._position);
569 oldItem.active(false);
570
571 this._position = this.limit() + this._offset - 1;
572 newItem = this.liveItem(this._position);
573 };
574
575 newItem.active(true);
576 },
577 */
578
Nils Diewald86dad5b2015-01-28 15:09:07 +0000579
580 /*
581 * Make the previous item in the menu active
582 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000583 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +0000584
Nils Diewald2fe12e12015-03-06 16:47:06 +0000585 // No active element set
Nils Diewalde8518f82015-03-18 22:41:49 +0000586 if (this._position === -1) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000587 return;
Nils Diewalde8518f82015-03-18 22:41:49 +0000588 // TODO: Choose last item
589 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000590
Nils Diewald5975d702015-03-09 17:45:42 +0000591 var newItem;
592
Nils Diewald86dad5b2015-01-28 15:09:07 +0000593 // Set new live item
Nils Diewald2d210752015-03-09 19:01:15 +0000594 if (!this._prefix.active()) {
595 var oldItem = this.liveItem(this._position--);
596 oldItem.active(false);
597 };
598
Nils Diewald5975d702015-03-09 17:45:42 +0000599 newItem = this.liveItem(this._position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000600
601 // The previous element is undefined - roll to bottom
602 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000603
604 // Activate prefix
605 var prefix = this._prefix;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000606 this._offset = this.liveLength() - this.limit();
Nils Diewald2d210752015-03-09 19:01:15 +0000607
608 // Normalize offset
609 this._offset = this._offset < 0 ? 0 : this._offset;
610
Nils Diewald2fe12e12015-03-06 16:47:06 +0000611 this._position = this.liveLength() - 1;
Nils Diewald5975d702015-03-09 17:45:42 +0000612
613 if (prefix.isSet() && !prefix.active()) {
614 this._position++;
615 prefix.active(true);
616 return;
617 }
618 else {
619 newItem = this.liveItem(this._position);
620 this._showItems(this._offset);
621 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000622 }
623
624 // The previous element is outside the view - roll up
625 else if (this._position < this._offset) {
626 this._removeLast();
627 this._offset--;
628 this._prepend(this._list[this._position]);
629 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000630
Nils Diewald5975d702015-03-09 17:45:42 +0000631 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000632 newItem.active(true);
633 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000634
635
Nils Diewald5975d702015-03-09 17:45:42 +0000636 // Length of the filtered list
637 liveLength : function () {
638 if (this._list === undefined)
639 this._initList();
640 return this._list.length;
641 },
642
643
Nils Diewald2fe12e12015-03-06 16:47:06 +0000644 // Remove the HTML node from the first item
Nils Diewald86dad5b2015-01-28 15:09:07 +0000645 _removeFirst : function () {
646 this.item(this._list[this._offset]).lowlight();
Nils Diewald5975d702015-03-09 17:45:42 +0000647 this._element.removeChild(this._element.children[1]);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000648 },
649
Nils Diewald2fe12e12015-03-06 16:47:06 +0000650
651 // Remove the HTML node from the last item
Nils Diewald86dad5b2015-01-28 15:09:07 +0000652 _removeLast : function () {
Nils Diewald2fe12e12015-03-06 16:47:06 +0000653 this.item(this._list[this._offset + this.limit() - 1]).lowlight();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000654 this._element.removeChild(this._element.lastChild);
Nils Diewald2fe12e12015-03-06 16:47:06 +0000655 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000656 };
657
658
Nils Diewaldfda29d92015-01-22 17:28:01 +0000659 /**
660 * Item in the Dropdown menu
661 */
662 KorAP.MenuItem = {
663
664 /**
665 * Create a new MenuItem object.
666 *
667 * @constructor
668 * @this {MenuItem}
669 * @param {Array.<string>} An array object of name, action and
670 * optionally a description
671 */
672 create : function (params) {
673 return Object.create(KorAP.MenuItem)._init(params);
674 },
675
676 /**
677 * Upgrade this object to another object,
678 * while private data stays intact.
679 *
680 * @param {Object] An object with properties.
681 */
682 upgradeTo : function (props) {
683 for (var prop in props) {
684 this[prop] = props[prop];
685 };
686 return this;
687 },
688
689 content : function (content) {
690 if (arguments.length === 1)
691 this._content = document.createTextNode(content);
692 return this._content;
693 },
694
695 lcField : function () {
696 return this._lcField;
697 },
698
699 action : function (action) {
700 if (arguments.length === 1)
701 this._action = action;
702 return this._action;
703 },
704
705 /**
706 * Check or set if the item is active
707 *
708 * @param {boolean|null} State of activity
709 */
710 active : function (bool) {
711 var cl = this.element().classList;
712 if (bool === undefined)
713 return cl.contains("active");
714 else if (bool)
715 cl.add("active");
716 else
717 cl.remove("active");
718 },
719
720 /**
721 * Check or set if the item is
722 * at the boundary of the menu
723 * list
724 *
725 * @param {boolean|null} State of activity
726 */
727 noMore : function (bool) {
728 var cl = this.element().classList;
729 if (bool === undefined)
730 return cl.contains("no-more");
731 else if (bool)
732 cl.add("no-more");
733 else
734 cl.remove("no-more");
735 },
736
737 /**
738 * Get the document element of the menu item
739 */
740 element : function () {
741 // already defined
742 if (this._element !== undefined)
743 return this._element;
744
745 // Create list item
746 var li = document.createElement("li");
747
748 // Connect action
Nils Diewald5c5a7472015-04-02 22:13:38 +0000749 if (this["onclick"] !== undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000750 li["onclick"] = this.onclick.bind(this);
Nils Diewald5c5a7472015-04-02 22:13:38 +0000751 };
Nils Diewaldfda29d92015-01-22 17:28:01 +0000752
753 // Append template
754 li.appendChild(this.content());
755
756 return this._element = li;
757 },
758
759 /**
760 * Highlight parts of the item
761 *
762 * @param {string} Prefix string for highlights
763 */
764 highlight : function (prefix) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000765 var children = this.element().childNodes;
766 for (var i = children.length -1; i >= 0; i--) {
767 this._highlight(children[i], prefix);
768 };
Nils Diewaldfda29d92015-01-22 17:28:01 +0000769 },
770
771 // Highlight a certain substring of the menu item
772 _highlight : function (elem, prefix) {
773
774 if (elem.nodeType === 3) {
775
776 var text = elem.nodeValue;
777 var textlc = text.toLowerCase();
778 var pos = textlc.indexOf(prefix);
779 if (pos >= 0) {
780
781 // First element
782 if (pos > 0) {
783 elem.parentNode.insertBefore(
784 document.createTextNode(text.substr(0, pos)),
785 elem
786 );
787 };
788
789 // Second element
790 var hl = document.createElement("mark");
791 hl.appendChild(
792 document.createTextNode(text.substr(pos, prefix.length))
793 );
794 elem.parentNode.insertBefore(hl, elem);
795
796 // Third element
797 var third = text.substr(pos + prefix.length);
798 if (third.length > 0) {
799 var thirdE = document.createTextNode(third);
800 elem.parentNode.insertBefore(
801 thirdE,
802 elem
803 );
804 this._highlight(thirdE, prefix);
805 };
806
807 var p = elem.parentNode;
808 p.removeChild(elem);
809 };
810 }
811 else {
812 var children = elem.childNodes;
813 for (var i = children.length -1; i >= 0; i--) {
814 this._highlight(children[i], prefix);
815 };
816 };
817 },
818
819
820 /**
821 * Remove highlight of the menu item
822 */
823 lowlight : function () {
824 var e = this.element();
825
826 var marks = e.getElementsByTagName("mark");
827 for (var i = marks.length - 1; i >= 0; i--) {
828 // Create text node clone
829 var x = document.createTextNode(
830 marks[i].firstChild.nodeValue
831 );
832
833 // Replace with content
834 marks[i].parentNode.replaceChild(
835 x,
836 marks[i]
837 );
838 };
839
840 // Remove consecutive textnodes
841 e.normalize();
842 },
843
844 // Initialize menu item
845 _init : function (params) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000846
Nils Diewaldfda29d92015-01-22 17:28:01 +0000847 if (params[0] === undefined)
848 throw new Error("Missing parameters");
849
850 this.content(params[0]);
851
852 if (params.length === 2)
853 this._action = params[1];
854
855 this._lcField = ' ' + this.content().textContent.toLowerCase();
856
857 return this;
858 },
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000859
860 /**
861 * Return menu list.
862 */
863 menu : function () {
864 return this._menu;
865 }
Nils Diewaldfda29d92015-01-22 17:28:01 +0000866 };
867
Nils Diewald5975d702015-03-09 17:45:42 +0000868 KorAP.MenuPrefix = {
869 create : function (params) {
870 return Object.create(KorAP.MenuPrefix)._init();
871 },
872 _init : function () {
873 this._string = '';
874
875 // Add prefix span
876 this._element = document.createElement('span');
877 this._element.classList.add('pref');
Nils Diewalde8518f82015-03-18 22:41:49 +0000878 // Connect action
Nils Diewald5c5a7472015-04-02 22:13:38 +0000879
880 if (this["onclick"] !== undefined)
Nils Diewalde8518f82015-03-18 22:41:49 +0000881 this._element["onclick"] = this.onclick.bind(this);
882
Nils Diewald5975d702015-03-09 17:45:42 +0000883 return this;
884 },
885 _update : function () {
886 this._element.innerHTML
887 = this._string;
888 },
889
890 /**
891 * Upgrade this object to another object,
892 * while private data stays intact.
893 *
894 * @param {Object} An object with properties.
895 */
896 upgradeTo : function (props) {
897 for (var prop in props) {
898 this[prop] = props[prop];
899 };
900 return this;
901 },
902
903 active : function (bool) {
904 var cl = this.element().classList;
905 if (bool === undefined)
906 return cl.contains("active");
907 else if (bool)
908 cl.add("active");
909 else
910 cl.remove("active");
911 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000912
Nils Diewald5975d702015-03-09 17:45:42 +0000913 element : function () {
914 return this._element;
915 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000916
Nils Diewald5975d702015-03-09 17:45:42 +0000917 isSet : function () {
918 return this._string.length > 0 ?
919 true : false;
920 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000921
Nils Diewald5975d702015-03-09 17:45:42 +0000922 value : function (string) {
923 if (arguments.length === 1) {
924 this._string = string;
925 this._update();
926 };
927 return this._string;
928 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000929
Nils Diewald5975d702015-03-09 17:45:42 +0000930 add : function (string) {
931 this._string += string;
932 this._update();
933 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000934
935 onclick : function () {},
936
Nils Diewald5975d702015-03-09 17:45:42 +0000937 backspace : function () {
938 if (this._string.length > 1) {
939 this._string = this._string.substring(
940 0, this._string.length - 1
941 );
942 }
943 else {
944 this._string = '';
945 };
946
947 this._update();
Nils Diewald5c5a7472015-04-02 22:13:38 +0000948 },
949
950 /**
951 * Return menu list.
952 */
953 menu : function () {
954 return this._menu;
Nils Diewald5975d702015-03-09 17:45:42 +0000955 }
956 };
957
Nils Diewald59c02fc2015-03-07 01:29:09 +0000958 function _codeFromEvent (e) {
Nils Diewald5975d702015-03-09 17:45:42 +0000959 if (e.charCode && (e.keyCode == 0))
Nils Diewald59c02fc2015-03-07 01:29:09 +0000960 return e.charCode
961 return e.keyCode;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000962 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000963
Nils Diewaldfda29d92015-01-22 17:28:01 +0000964}(this.KorAP));
Nils Diewald5975d702015-03-09 17:45:42 +0000965
966/**
967 * MenuItems may define:
968 *
969 * onclick: action happen on click and enter.
970 * further: action happen on right arrow
971 */