blob: 175360ad69e04308709d0110abcd8df3fa99a308 [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
13 Event.prototype.halt = function () {
14 this.stopPropagation();
15 this.preventDefault();
16 };
17
Nils Diewald86dad5b2015-01-28 15:09:07 +000018 // Default maximum number of menu items
19 KorAP.menuLimit = 8;
20
21 /**
22 * List of items for drop down menu (complete).
23 * Only a sublist of the menu is filtered (live).
24 * Only a sublist of the filtered menu is visible (shown).
25 */
26 KorAP.Menu = {
27 /**
28 * Create new Menu based on the action prefix
29 * and a list of menu items.
30 *
31 * @this {Menu}
32 * @constructor
33 * @param {string} Context prefix
34 * @param {Array.<Array.<string>>} List of menu items
35 */
36 create : function (params) {
37 return Object.create(KorAP.Menu)._init(params);
38 },
39
Nils Diewald2fe12e12015-03-06 16:47:06 +000040 focus : function () {
41 this._element.focus();
42 },
43
Nils Diewald59c02fc2015-03-07 01:29:09 +000044 // mouse wheel treatment
45 _mousewheel : function (e) {
46 var delta = 0;
47 if (e.wheelDelta) {
48 delta = event.wheelDelta / 120;
49 }
50 else if (e.detail) {
51 delta = - e.detail / 3;
52 };
53 if (delta < 0) {
54 this.next();
55 }
56 else {
57 this.prev();
58 };
59 e.halt();
60 },
61
62 // Arrow key and prefix treatment
63 _keydown : function (e) {
64 var code = _codeFromEvent(e);
65
66 /*
67 * keyCodes:
68 * - Down = 40
69 * - Esc = 27
70 * - Up = 38
71 * - Enter = 13
72 * - shift = 16
73 * for characters use e.key
74 */
75
76 switch (code) {
77 case 27: // 'Esc'
78 e.halt();
79 this.hide();
80 break;
81 case 40: // 'Down'
82 e.halt();
83 this.next();
84 break;
85 case 38: // 'Up'
86 e.halt();
87 this.prev();
88 break;
89 case 13: // 'Enter'
90 console.log('hide');
91 e.halt();
92 this.hide();
93 break;
94 case 8: // 'Backspace'
95 var p = this.prefix();
96 if (p.length > 1) {
97 p = p.substring(0, p.length - 1)
98 this.show(p);
99 }
100 else {
101 this.show();
102 };
103 e.halt();
104 break;
105 default:
106 if (e.key !== undefined && e.key.length != 1)
107 return;
108
109 // Add prefix
110 if (!this.show(this.prefix() + e.key))
111 this.hide();
112 };
113 },
114
Nils Diewald2fe12e12015-03-06 16:47:06 +0000115 // Initialize list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000116 _init : function (itemClass, params) {
117 // this._element.addEventListener("click", chooseHint, false);
Nils Diewald59c02fc2015-03-07 01:29:09 +0000118 var that = this;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000119 this._itemClass = itemClass;
Nils Diewald59c02fc2015-03-07 01:29:09 +0000120 var e =this._element = document.createElement("ul");
121 e.style.opacity = 0;
122 e.style.outline = 0;
123 e.setAttribute('tabindex', 0);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000124
Nils Diewald59c02fc2015-03-07 01:29:09 +0000125 // Arrow keys
126 e.addEventListener(
Nils Diewald2fe12e12015-03-06 16:47:06 +0000127 "keydown",
Nils Diewald59c02fc2015-03-07 01:29:09 +0000128 function (ev) {
129 that._keydown(ev)
130 },
131 false
132 );
133
134 // Mousewheel
135 e.addEventListener(
136 'DOMMouseScroll',
137 function (ev) {
138 that._mousewheel(ev)
Nils Diewald2fe12e12015-03-06 16:47:06 +0000139 },
140 false
141 );
142
Nils Diewald86dad5b2015-01-28 15:09:07 +0000143 this.active = false;
144 this._items = new Array();
145 var i;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000146
147 // Initialize item list based on parameters
Nils Diewald86dad5b2015-01-28 15:09:07 +0000148 for (i in params) {
149 var obj = itemClass.create(params[i]);
Nils Diewald2fe12e12015-03-06 16:47:06 +0000150 this._items.push(obj);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000151 };
152 this._limit = KorAP.menuLimit;
153 this._position = 0; // position in the active list
154 this._active = -1; // active item in the item list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000155 this._reset();
156 return this;
157 },
158
Nils Diewald2fe12e12015-03-06 16:47:06 +0000159 /**
160 * Get the instantiated HTML element
161 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000162 element : function () {
163 return this._element;
164 },
165
Nils Diewald2fe12e12015-03-06 16:47:06 +0000166 /**
167 * Get the creator object for items
168 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000169 itemClass : function () {
170 return this._itemClass;
171 },
172
173 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000174 * Get and set numerical value for limit,
175 * i.e. the number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000176 */
177 limit : function (limit) {
178 if (arguments.length === 1)
179 this._limit = limit;
180 return this._limit;
181 },
182
183 /**
184 * Upgrade this object to another object,
185 * while private data stays intact.
186 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000187 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000188 */
189 upgradeTo : function (props) {
190 for (var prop in props) {
191 this[prop] = props[prop];
192 };
193 return this;
194 },
195
Nils Diewald2fe12e12015-03-06 16:47:06 +0000196 // Reset chosen item and prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000197 _reset : function () {
198 this._offset = 0;
199 this._pos = 0;
200 this._prefix = undefined;
201 },
202
203 /**
204 * Filter the list and make it visible
205 *
206 * @param {string} Prefix for filtering the list
207 */
208 show : function (prefix) {
209 this._prefix = prefix;
210
211 // Initialize the list
212 if (!this._initList())
Nils Diewald59c02fc2015-03-07 01:29:09 +0000213 return false;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000214
Nils Diewald2fe12e12015-03-06 16:47:06 +0000215 // show based on initial offset
Nils Diewald86dad5b2015-01-28 15:09:07 +0000216 this._showItems(0);
217
218 // Set the first element to active
Nils Diewald2fe12e12015-03-06 16:47:06 +0000219 // Todo: Or the last element chosen
Nils Diewald86dad5b2015-01-28 15:09:07 +0000220 this.liveItem(0).active(true);
221
222 this._position = 0;
223 this._active = this._list[0];
224
Nils Diewald2fe12e12015-03-06 16:47:06 +0000225 this._element.style.opacity = 1;
226
Nils Diewald86dad5b2015-01-28 15:09:07 +0000227 // Add classes for rolling menus
228 this._boundary(true);
Nils Diewald59c02fc2015-03-07 01:29:09 +0000229 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000230 },
231
Nils Diewald2fe12e12015-03-06 16:47:06 +0000232 hide : function () {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000233 this.active = false;
234 this.delete();
Nils Diewald2fe12e12015-03-06 16:47:06 +0000235 this._element.style.opacity = 0;
Nils Diewald59c02fc2015-03-07 01:29:09 +0000236
237/*
238 this._element.blur();
239*/
Nils Diewald86dad5b2015-01-28 15:09:07 +0000240 },
241
Nils Diewald2fe12e12015-03-06 16:47:06 +0000242 // Initialize the list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000243 _initList : function () {
244
Nils Diewald2fe12e12015-03-06 16:47:06 +0000245 // Create a new list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000246 if (this._list === undefined) {
247 this._list = [];
248 }
249 else if (this._list.length != 0) {
250 this._boundary(false);
251 this._list.length = 0;
252 };
253
254 // Offset is initially zero
255 this._offset = 0;
256
Nils Diewald2fe12e12015-03-06 16:47:06 +0000257 // There is no prefix set
Nils Diewald86dad5b2015-01-28 15:09:07 +0000258 if (this.prefix().length <= 0) {
259 for (var i = 0; i < this._items.length; i++)
260 this._list.push(i);
261 return true;
262 };
263
Nils Diewald2fe12e12015-03-06 16:47:06 +0000264 // There is a prefix set, so filter the list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000265 var pos;
266 var paddedPrefix = " " + this.prefix();
267
Nils Diewald2fe12e12015-03-06 16:47:06 +0000268 // Iterate over all items and choose preferred matching items
269 // i.e. the matching happens at the word start
Nils Diewald86dad5b2015-01-28 15:09:07 +0000270 for (pos = 0; pos < this._items.length; pos++) {
271 if ((this.item(pos).lcField().indexOf(paddedPrefix)) >= 0)
272 this._list.push(pos);
273 };
274
Nils Diewald2fe12e12015-03-06 16:47:06 +0000275 // The list is empty - so lower your expectations
276 // Iterate over all items and choose matching items
277 // i.e. the matching happens anywhere in the word
Nils Diewald86dad5b2015-01-28 15:09:07 +0000278 if (this._list.length == 0) {
279 for (pos = 0; pos < this._items.length; pos++) {
280 if ((this.item(pos).lcField().indexOf(this.prefix())) >= 0)
281 this._list.push(pos);
282 };
283 };
284
Nils Diewald2fe12e12015-03-06 16:47:06 +0000285 // Filter was successful - yeah!
Nils Diewald86dad5b2015-01-28 15:09:07 +0000286 return this._list.length > 0 ? true : false;
287 },
288
289 // Set boundary for viewport
290 _boundary : function (bool) {
291 this.item(this._list[0]).noMore(bool);
292 this.item(this._list[this._list.length - 1]).noMore(bool);
293 },
294
295 /**
296 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000297 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000298 */
299 prefix : function () {
300 return this._prefix || '';
301 },
302
Nils Diewald2fe12e12015-03-06 16:47:06 +0000303 // Append Items that should be shown
Nils Diewald86dad5b2015-01-28 15:09:07 +0000304 _showItems : function (offset) {
305 this.delete();
306
307 // Use list
308 var shown = 0;
309 var i;
310 for (i in this._list) {
311
312 // Don't show - it's before offset
313 if (shown++ < offset)
314 continue;
315
316 this._append(this._list[i]);
317
318 if (shown >= (this.limit() + this._offset))
319 break;
320 };
321 },
322
323 /**
324 * Delete all visible items from the menu element
325 */
326 delete : function () {
327 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000328
329 // Iterate over all visible items
Nils Diewald86dad5b2015-01-28 15:09:07 +0000330 for (var i = 0; i <= this.limit(); i++) {
331
Nils Diewald2fe12e12015-03-06 16:47:06 +0000332 // there is a visible element - unhighlight!
Nils Diewald59c02fc2015-03-07 01:29:09 +0000333 if (child = this.shownItem(i)) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000334 child.lowlight();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000335 child.active(false);
336 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000337 };
338
Nils Diewald2fe12e12015-03-06 16:47:06 +0000339 // Remove all children
Nils Diewald86dad5b2015-01-28 15:09:07 +0000340 while (child = this._element.firstChild)
341 this._element.removeChild(child);
342 },
343
344
345 // Append item to the shown list based on index
346 _append : function (i) {
347 var item = this.item(i);
348
349 // Highlight based on prefix
350 if (this.prefix().length > 0)
351 item.highlight(this.prefix());
352
353 // Append element
354 this.element().appendChild(item.element());
355 },
356
357
Nils Diewald2fe12e12015-03-06 16:47:06 +0000358 // Prepend item to the shown list based on index
359 _prepend : function (i) {
360 var item = this.item(i);
361
362 // Highlight based on prefix
363 if (this.prefix().length > 0)
364 item.highlight(this.prefix());
365
366 var e = this.element();
367 // Append element
368 e.insertBefore(
369 item.element(),
370 e.firstChild
371 );
372 },
373
374
375 /**
376 * Get a specific item from the complete list
377 *
378 * @param {number} index of the list item
379 */
380 item : function (index) {
381 return this._items[index]
382 },
383
384
Nils Diewald86dad5b2015-01-28 15:09:07 +0000385 /**
386 * Get a specific item from the filtered list
387 *
388 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000389 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000390 */
391 liveItem : function (index) {
392 if (this._list === undefined)
393 if (!this._initList())
394 return;
395
396 return this._items[this._list[index]];
397 },
398
Nils Diewald86dad5b2015-01-28 15:09:07 +0000399
400 /**
401 * Get a specific item from the visible list
402 *
403 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000404 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000405 */
406 shownItem : function (index) {
407 if (index >= this.limit())
408 return;
409 return this.liveItem(this._offset + index);
410 },
411
412
Nils Diewald2fe12e12015-03-06 16:47:06 +0000413 /**
414 * Get the length of the full list
415 */
416 length : function () {
417 return this._items.length;
418 },
419
420
421 /**
422 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000423 */
424 next : function () {
425 // No active element set
426 if (this._position == -1)
427 return;
428
429 // Set new live item
430 var oldItem = this.liveItem(this._position++);
431 oldItem.active(false);
432 var newItem = this.liveItem(this._position);
433
434 // The next element is undefined - roll to top
435 if (newItem === undefined) {
436 this._offset = 0;
437 this._position = 0;
438 newItem = this.liveItem(0);
439 this._showItems(0);
440 }
441
442 // The next element is outside the view - roll down
Nils Diewald2fe12e12015-03-06 16:47:06 +0000443 else if (this._position >= (this.limit() + this._offset)) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000444 this._removeFirst();
445 this._offset++;
446 this._append(this._list[this._position]);
447 };
448 newItem.active(true);
449 },
450
451
452 /*
453 * Make the previous item in the menu active
454 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000455 prev : function () {
Nils Diewald2fe12e12015-03-06 16:47:06 +0000456 // No active element set
Nils Diewald86dad5b2015-01-28 15:09:07 +0000457 if (this._position == -1)
458 return;
459
460 // Set new live item
461 var oldItem = this.liveItem(this._position--);
462 oldItem.active(false);
463 var newItem = this.liveItem(this._position);
464
465 // The previous element is undefined - roll to bottom
466 if (newItem === undefined) {
Nils Diewald2fe12e12015-03-06 16:47:06 +0000467 this._offset = this.liveLength() - this.limit();
468 this._position = this.liveLength() - 1;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000469 newItem = this.liveItem(this._position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000470 this._showItems(this._offset);
471 }
472
473 // The previous element is outside the view - roll up
474 else if (this._position < this._offset) {
475 this._removeLast();
476 this._offset--;
477 this._prepend(this._list[this._position]);
478 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000479
Nils Diewald86dad5b2015-01-28 15:09:07 +0000480 newItem.active(true);
481 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000482
483
Nils Diewald2fe12e12015-03-06 16:47:06 +0000484 // Remove the HTML node from the first item
Nils Diewald86dad5b2015-01-28 15:09:07 +0000485 _removeFirst : function () {
486 this.item(this._list[this._offset]).lowlight();
487 this._element.removeChild(this._element.firstChild);
488 },
489
Nils Diewald2fe12e12015-03-06 16:47:06 +0000490
491 // Remove the HTML node from the last item
Nils Diewald86dad5b2015-01-28 15:09:07 +0000492 _removeLast : function () {
Nils Diewald2fe12e12015-03-06 16:47:06 +0000493 this.item(this._list[this._offset + this.limit() - 1]).lowlight();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000494 this._element.removeChild(this._element.lastChild);
495 },
496
Nils Diewald2fe12e12015-03-06 16:47:06 +0000497 // Length of the filtered list
498 liveLength : function () {
499 if (this._list === undefined)
500 this._initList();
501 return this._list.length;
502 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000503 };
504
505
Nils Diewaldfda29d92015-01-22 17:28:01 +0000506 /**
507 * Item in the Dropdown menu
508 */
509 KorAP.MenuItem = {
510
511 /**
512 * Create a new MenuItem object.
513 *
514 * @constructor
515 * @this {MenuItem}
516 * @param {Array.<string>} An array object of name, action and
517 * optionally a description
518 */
519 create : function (params) {
520 return Object.create(KorAP.MenuItem)._init(params);
521 },
522
523 /**
524 * Upgrade this object to another object,
525 * while private data stays intact.
526 *
527 * @param {Object] An object with properties.
528 */
529 upgradeTo : function (props) {
530 for (var prop in props) {
531 this[prop] = props[prop];
532 };
533 return this;
534 },
535
536 content : function (content) {
537 if (arguments.length === 1)
538 this._content = document.createTextNode(content);
539 return this._content;
540 },
541
542 lcField : function () {
543 return this._lcField;
544 },
545
546 action : function (action) {
547 if (arguments.length === 1)
548 this._action = action;
549 return this._action;
550 },
551
552 /**
553 * Check or set if the item is active
554 *
555 * @param {boolean|null} State of activity
556 */
557 active : function (bool) {
558 var cl = this.element().classList;
559 if (bool === undefined)
560 return cl.contains("active");
561 else if (bool)
562 cl.add("active");
563 else
564 cl.remove("active");
565 },
566
567 /**
568 * Check or set if the item is
569 * at the boundary of the menu
570 * list
571 *
572 * @param {boolean|null} State of activity
573 */
574 noMore : function (bool) {
575 var cl = this.element().classList;
576 if (bool === undefined)
577 return cl.contains("no-more");
578 else if (bool)
579 cl.add("no-more");
580 else
581 cl.remove("no-more");
582 },
583
584 /**
585 * Get the document element of the menu item
586 */
587 element : function () {
588 // already defined
589 if (this._element !== undefined)
590 return this._element;
591
592 // Create list item
593 var li = document.createElement("li");
594
595 // Connect action
596 li["action"] = this._action;
597
598 // Append template
599 li.appendChild(this.content());
600
601 return this._element = li;
602 },
603
604 /**
605 * Highlight parts of the item
606 *
607 * @param {string} Prefix string for highlights
608 */
609 highlight : function (prefix) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000610 var children = this.element().childNodes;
611 for (var i = children.length -1; i >= 0; i--) {
612 this._highlight(children[i], prefix);
613 };
Nils Diewaldfda29d92015-01-22 17:28:01 +0000614 },
615
616 // Highlight a certain substring of the menu item
617 _highlight : function (elem, prefix) {
618
619 if (elem.nodeType === 3) {
620
621 var text = elem.nodeValue;
622 var textlc = text.toLowerCase();
623 var pos = textlc.indexOf(prefix);
624 if (pos >= 0) {
625
626 // First element
627 if (pos > 0) {
628 elem.parentNode.insertBefore(
629 document.createTextNode(text.substr(0, pos)),
630 elem
631 );
632 };
633
634 // Second element
635 var hl = document.createElement("mark");
636 hl.appendChild(
637 document.createTextNode(text.substr(pos, prefix.length))
638 );
639 elem.parentNode.insertBefore(hl, elem);
640
641 // Third element
642 var third = text.substr(pos + prefix.length);
643 if (third.length > 0) {
644 var thirdE = document.createTextNode(third);
645 elem.parentNode.insertBefore(
646 thirdE,
647 elem
648 );
649 this._highlight(thirdE, prefix);
650 };
651
652 var p = elem.parentNode;
653 p.removeChild(elem);
654 };
655 }
656 else {
657 var children = elem.childNodes;
658 for (var i = children.length -1; i >= 0; i--) {
659 this._highlight(children[i], prefix);
660 };
661 };
662 },
663
664
665 /**
666 * Remove highlight of the menu item
667 */
668 lowlight : function () {
669 var e = this.element();
670
671 var marks = e.getElementsByTagName("mark");
672 for (var i = marks.length - 1; i >= 0; i--) {
673 // Create text node clone
674 var x = document.createTextNode(
675 marks[i].firstChild.nodeValue
676 );
677
678 // Replace with content
679 marks[i].parentNode.replaceChild(
680 x,
681 marks[i]
682 );
683 };
684
685 // Remove consecutive textnodes
686 e.normalize();
687 },
688
689 // Initialize menu item
690 _init : function (params) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000691
Nils Diewaldfda29d92015-01-22 17:28:01 +0000692 if (params[0] === undefined)
693 throw new Error("Missing parameters");
694
695 this.content(params[0]);
696
697 if (params.length === 2)
698 this._action = params[1];
699
700 this._lcField = ' ' + this.content().textContent.toLowerCase();
701
702 return this;
703 },
704 };
705
Nils Diewald59c02fc2015-03-07 01:29:09 +0000706 function _codeFromEvent (e) {
707 if ((e.charCode) && (e.keyCode==0))
708 return e.charCode
709 return e.keyCode;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000710 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000711
Nils Diewaldfda29d92015-01-22 17:28:01 +0000712}(this.KorAP));