blob: f0dd15dd7e9a8a687f89e0355373a16d6bf3cd0a [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 Diewald86dad5b2015-01-28 15:09:07 +000012 // Default maximum number of menu items
13 KorAP.menuLimit = 8;
14
15 /**
16 * List of items for drop down menu (complete).
17 * Only a sublist of the menu is filtered (live).
18 * Only a sublist of the filtered menu is visible (shown).
19 */
20 KorAP.Menu = {
21 /**
22 * Create new Menu based on the action prefix
23 * and a list of menu items.
24 *
25 * @this {Menu}
26 * @constructor
27 * @param {string} Context prefix
28 * @param {Array.<Array.<string>>} List of menu items
29 */
30 create : function (params) {
31 return Object.create(KorAP.Menu)._init(params);
32 },
33
Nils Diewald2fe12e12015-03-06 16:47:06 +000034 focus : function () {
35 this._element.focus();
36 },
37
38 // Initialize list
Nils Diewald86dad5b2015-01-28 15:09:07 +000039 _init : function (itemClass, params) {
40 // this._element.addEventListener("click", chooseHint, false);
41 this._itemClass = itemClass;
42 this._element = document.createElement("ul");
43 this._element.style.opacity = 0;
44
Nils Diewald2fe12e12015-03-06 16:47:06 +000045/*
46 this._listener = document.createElement('input');
47 this._listener.setAttribute('type', 'text');
48// this._listener.style.display = "none";
49*/
50 this._element.addEventListener(
51 "keydown",
52 function (e) {
53 console.log('+++');
54 },
55 false
56 );
57
Nils Diewald86dad5b2015-01-28 15:09:07 +000058 this.active = false;
59 this._items = new Array();
60 var i;
Nils Diewald2fe12e12015-03-06 16:47:06 +000061
62 // Initialize item list based on parameters
Nils Diewald86dad5b2015-01-28 15:09:07 +000063 for (i in params) {
64 var obj = itemClass.create(params[i]);
Nils Diewald2fe12e12015-03-06 16:47:06 +000065 this._items.push(obj);
Nils Diewald86dad5b2015-01-28 15:09:07 +000066 };
67 this._limit = KorAP.menuLimit;
68 this._position = 0; // position in the active list
69 this._active = -1; // active item in the item list
Nils Diewald86dad5b2015-01-28 15:09:07 +000070 this._reset();
71 return this;
72 },
73
Nils Diewald2fe12e12015-03-06 16:47:06 +000074 /**
75 * Get the instantiated HTML element
76 */
Nils Diewald86dad5b2015-01-28 15:09:07 +000077 element : function () {
78 return this._element;
79 },
80
Nils Diewald2fe12e12015-03-06 16:47:06 +000081 /**
82 * Get the creator object for items
83 */
Nils Diewald86dad5b2015-01-28 15:09:07 +000084 itemClass : function () {
85 return this._itemClass;
86 },
87
88 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +000089 * Get and set numerical value for limit,
90 * i.e. the number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +000091 */
92 limit : function (limit) {
93 if (arguments.length === 1)
94 this._limit = limit;
95 return this._limit;
96 },
97
98 /**
99 * Upgrade this object to another object,
100 * while private data stays intact.
101 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000102 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000103 */
104 upgradeTo : function (props) {
105 for (var prop in props) {
106 this[prop] = props[prop];
107 };
108 return this;
109 },
110
Nils Diewald2fe12e12015-03-06 16:47:06 +0000111 // Reset chosen item and prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000112 _reset : function () {
113 this._offset = 0;
114 this._pos = 0;
115 this._prefix = undefined;
116 },
117
118 /**
119 * Filter the list and make it visible
120 *
121 * @param {string} Prefix for filtering the list
122 */
123 show : function (prefix) {
124 this._prefix = prefix;
125
126 // Initialize the list
127 if (!this._initList())
128 return;
129
Nils Diewald2fe12e12015-03-06 16:47:06 +0000130 // show based on initial offset
Nils Diewald86dad5b2015-01-28 15:09:07 +0000131 this._showItems(0);
132
133 // Set the first element to active
Nils Diewald2fe12e12015-03-06 16:47:06 +0000134 // Todo: Or the last element chosen
Nils Diewald86dad5b2015-01-28 15:09:07 +0000135 this.liveItem(0).active(true);
136
137 this._position = 0;
138 this._active = this._list[0];
139
Nils Diewald2fe12e12015-03-06 16:47:06 +0000140 this._element.style.opacity = 1;
141
Nils Diewald86dad5b2015-01-28 15:09:07 +0000142 // Add classes for rolling menus
143 this._boundary(true);
144 },
145
Nils Diewald2fe12e12015-03-06 16:47:06 +0000146 hide : function () {
147 this._element.style.opacity = 0;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000148 },
149
Nils Diewald2fe12e12015-03-06 16:47:06 +0000150 // Initialize the list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000151 _initList : function () {
152
Nils Diewald2fe12e12015-03-06 16:47:06 +0000153 // Create a new list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000154 if (this._list === undefined) {
155 this._list = [];
156 }
157 else if (this._list.length != 0) {
158 this._boundary(false);
159 this._list.length = 0;
160 };
161
162 // Offset is initially zero
163 this._offset = 0;
164
Nils Diewald2fe12e12015-03-06 16:47:06 +0000165 // There is no prefix set
Nils Diewald86dad5b2015-01-28 15:09:07 +0000166 if (this.prefix().length <= 0) {
167 for (var i = 0; i < this._items.length; i++)
168 this._list.push(i);
169 return true;
170 };
171
Nils Diewald2fe12e12015-03-06 16:47:06 +0000172 // There is a prefix set, so filter the list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000173 var pos;
174 var paddedPrefix = " " + this.prefix();
175
Nils Diewald2fe12e12015-03-06 16:47:06 +0000176 // Iterate over all items and choose preferred matching items
177 // i.e. the matching happens at the word start
Nils Diewald86dad5b2015-01-28 15:09:07 +0000178 for (pos = 0; pos < this._items.length; pos++) {
179 if ((this.item(pos).lcField().indexOf(paddedPrefix)) >= 0)
180 this._list.push(pos);
181 };
182
Nils Diewald2fe12e12015-03-06 16:47:06 +0000183 // The list is empty - so lower your expectations
184 // Iterate over all items and choose matching items
185 // i.e. the matching happens anywhere in the word
Nils Diewald86dad5b2015-01-28 15:09:07 +0000186 if (this._list.length == 0) {
187 for (pos = 0; pos < this._items.length; pos++) {
188 if ((this.item(pos).lcField().indexOf(this.prefix())) >= 0)
189 this._list.push(pos);
190 };
191 };
192
Nils Diewald2fe12e12015-03-06 16:47:06 +0000193 // Filter was successful - yeah!
Nils Diewald86dad5b2015-01-28 15:09:07 +0000194 return this._list.length > 0 ? true : false;
195 },
196
197 // Set boundary for viewport
198 _boundary : function (bool) {
199 this.item(this._list[0]).noMore(bool);
200 this.item(this._list[this._list.length - 1]).noMore(bool);
201 },
202
203 /**
204 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000205 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000206 */
207 prefix : function () {
208 return this._prefix || '';
209 },
210
Nils Diewald2fe12e12015-03-06 16:47:06 +0000211 // Append Items that should be shown
Nils Diewald86dad5b2015-01-28 15:09:07 +0000212 _showItems : function (offset) {
213 this.delete();
214
215 // Use list
216 var shown = 0;
217 var i;
218 for (i in this._list) {
219
220 // Don't show - it's before offset
221 if (shown++ < offset)
222 continue;
223
224 this._append(this._list[i]);
225
226 if (shown >= (this.limit() + this._offset))
227 break;
228 };
229 },
230
231 /**
232 * Delete all visible items from the menu element
233 */
234 delete : function () {
235 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000236
237 // Iterate over all visible items
Nils Diewald86dad5b2015-01-28 15:09:07 +0000238 for (var i = 0; i <= this.limit(); i++) {
239
Nils Diewald2fe12e12015-03-06 16:47:06 +0000240 // there is a visible element - unhighlight!
Nils Diewald86dad5b2015-01-28 15:09:07 +0000241 if (child = this.shownItem(i))
242 child.lowlight();
243 };
244
Nils Diewald2fe12e12015-03-06 16:47:06 +0000245 // Remove all children
Nils Diewald86dad5b2015-01-28 15:09:07 +0000246 while (child = this._element.firstChild)
247 this._element.removeChild(child);
248 },
249
250
251 // Append item to the shown list based on index
252 _append : function (i) {
253 var item = this.item(i);
254
255 // Highlight based on prefix
256 if (this.prefix().length > 0)
257 item.highlight(this.prefix());
258
259 // Append element
260 this.element().appendChild(item.element());
261 },
262
263
Nils Diewald2fe12e12015-03-06 16:47:06 +0000264 // Prepend item to the shown list based on index
265 _prepend : function (i) {
266 var item = this.item(i);
267
268 // Highlight based on prefix
269 if (this.prefix().length > 0)
270 item.highlight(this.prefix());
271
272 var e = this.element();
273 // Append element
274 e.insertBefore(
275 item.element(),
276 e.firstChild
277 );
278 },
279
280
281 /**
282 * Get a specific item from the complete list
283 *
284 * @param {number} index of the list item
285 */
286 item : function (index) {
287 return this._items[index]
288 },
289
290
Nils Diewald86dad5b2015-01-28 15:09:07 +0000291 /**
292 * Get a specific item from the filtered list
293 *
294 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000295 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000296 */
297 liveItem : function (index) {
298 if (this._list === undefined)
299 if (!this._initList())
300 return;
301
302 return this._items[this._list[index]];
303 },
304
Nils Diewald86dad5b2015-01-28 15:09:07 +0000305
306 /**
307 * Get a specific item from the visible list
308 *
309 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000310 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000311 */
312 shownItem : function (index) {
313 if (index >= this.limit())
314 return;
315 return this.liveItem(this._offset + index);
316 },
317
318
Nils Diewald2fe12e12015-03-06 16:47:06 +0000319 /**
320 * Get the length of the full list
321 */
322 length : function () {
323 return this._items.length;
324 },
325
326
327 /**
328 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000329 */
330 next : function () {
331 // No active element set
332 if (this._position == -1)
333 return;
334
335 // Set new live item
336 var oldItem = this.liveItem(this._position++);
337 oldItem.active(false);
338 var newItem = this.liveItem(this._position);
339
340 // The next element is undefined - roll to top
341 if (newItem === undefined) {
342 this._offset = 0;
343 this._position = 0;
344 newItem = this.liveItem(0);
345 this._showItems(0);
346 }
347
348 // The next element is outside the view - roll down
Nils Diewald2fe12e12015-03-06 16:47:06 +0000349 else if (this._position >= (this.limit() + this._offset)) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000350 this._removeFirst();
351 this._offset++;
352 this._append(this._list[this._position]);
353 };
354 newItem.active(true);
355 },
356
357
358 /*
359 * Make the previous item in the menu active
360 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000361 prev : function () {
Nils Diewald2fe12e12015-03-06 16:47:06 +0000362 // No active element set
Nils Diewald86dad5b2015-01-28 15:09:07 +0000363 if (this._position == -1)
364 return;
365
366 // Set new live item
367 var oldItem = this.liveItem(this._position--);
368 oldItem.active(false);
369 var newItem = this.liveItem(this._position);
370
371 // The previous element is undefined - roll to bottom
372 if (newItem === undefined) {
Nils Diewald2fe12e12015-03-06 16:47:06 +0000373 this._offset = this.liveLength() - this.limit();
374 this._position = this.liveLength() - 1;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000375 newItem = this.liveItem(this._position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000376 this._showItems(this._offset);
377 }
378
379 // The previous element is outside the view - roll up
380 else if (this._position < this._offset) {
381 this._removeLast();
382 this._offset--;
383 this._prepend(this._list[this._position]);
384 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000385
Nils Diewald86dad5b2015-01-28 15:09:07 +0000386 newItem.active(true);
387 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000388
389
Nils Diewald2fe12e12015-03-06 16:47:06 +0000390 // Remove the HTML node from the first item
Nils Diewald86dad5b2015-01-28 15:09:07 +0000391 _removeFirst : function () {
392 this.item(this._list[this._offset]).lowlight();
393 this._element.removeChild(this._element.firstChild);
394 },
395
Nils Diewald2fe12e12015-03-06 16:47:06 +0000396
397 // Remove the HTML node from the last item
Nils Diewald86dad5b2015-01-28 15:09:07 +0000398 _removeLast : function () {
Nils Diewald2fe12e12015-03-06 16:47:06 +0000399 this.item(this._list[this._offset + this.limit() - 1]).lowlight();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000400 this._element.removeChild(this._element.lastChild);
401 },
402
Nils Diewald2fe12e12015-03-06 16:47:06 +0000403 // Length of the filtered list
404 liveLength : function () {
405 if (this._list === undefined)
406 this._initList();
407 return this._list.length;
408 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000409 };
410
411
Nils Diewaldfda29d92015-01-22 17:28:01 +0000412 /**
413 * Item in the Dropdown menu
414 */
415 KorAP.MenuItem = {
416
417 /**
418 * Create a new MenuItem object.
419 *
420 * @constructor
421 * @this {MenuItem}
422 * @param {Array.<string>} An array object of name, action and
423 * optionally a description
424 */
425 create : function (params) {
426 return Object.create(KorAP.MenuItem)._init(params);
427 },
428
429 /**
430 * Upgrade this object to another object,
431 * while private data stays intact.
432 *
433 * @param {Object] An object with properties.
434 */
435 upgradeTo : function (props) {
436 for (var prop in props) {
437 this[prop] = props[prop];
438 };
439 return this;
440 },
441
442 content : function (content) {
443 if (arguments.length === 1)
444 this._content = document.createTextNode(content);
445 return this._content;
446 },
447
448 lcField : function () {
449 return this._lcField;
450 },
451
452 action : function (action) {
453 if (arguments.length === 1)
454 this._action = action;
455 return this._action;
456 },
457
458 /**
459 * Check or set if the item is active
460 *
461 * @param {boolean|null} State of activity
462 */
463 active : function (bool) {
464 var cl = this.element().classList;
465 if (bool === undefined)
466 return cl.contains("active");
467 else if (bool)
468 cl.add("active");
469 else
470 cl.remove("active");
471 },
472
473 /**
474 * Check or set if the item is
475 * at the boundary of the menu
476 * list
477 *
478 * @param {boolean|null} State of activity
479 */
480 noMore : function (bool) {
481 var cl = this.element().classList;
482 if (bool === undefined)
483 return cl.contains("no-more");
484 else if (bool)
485 cl.add("no-more");
486 else
487 cl.remove("no-more");
488 },
489
490 /**
491 * Get the document element of the menu item
492 */
493 element : function () {
494 // already defined
495 if (this._element !== undefined)
496 return this._element;
497
498 // Create list item
499 var li = document.createElement("li");
500
501 // Connect action
502 li["action"] = this._action;
503
504 // Append template
505 li.appendChild(this.content());
506
507 return this._element = li;
508 },
509
510 /**
511 * Highlight parts of the item
512 *
513 * @param {string} Prefix string for highlights
514 */
515 highlight : function (prefix) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000516 var children = this.element().childNodes;
517 for (var i = children.length -1; i >= 0; i--) {
518 this._highlight(children[i], prefix);
519 };
Nils Diewaldfda29d92015-01-22 17:28:01 +0000520 },
521
522 // Highlight a certain substring of the menu item
523 _highlight : function (elem, prefix) {
524
525 if (elem.nodeType === 3) {
526
527 var text = elem.nodeValue;
528 var textlc = text.toLowerCase();
529 var pos = textlc.indexOf(prefix);
530 if (pos >= 0) {
531
532 // First element
533 if (pos > 0) {
534 elem.parentNode.insertBefore(
535 document.createTextNode(text.substr(0, pos)),
536 elem
537 );
538 };
539
540 // Second element
541 var hl = document.createElement("mark");
542 hl.appendChild(
543 document.createTextNode(text.substr(pos, prefix.length))
544 );
545 elem.parentNode.insertBefore(hl, elem);
546
547 // Third element
548 var third = text.substr(pos + prefix.length);
549 if (third.length > 0) {
550 var thirdE = document.createTextNode(third);
551 elem.parentNode.insertBefore(
552 thirdE,
553 elem
554 );
555 this._highlight(thirdE, prefix);
556 };
557
558 var p = elem.parentNode;
559 p.removeChild(elem);
560 };
561 }
562 else {
563 var children = elem.childNodes;
564 for (var i = children.length -1; i >= 0; i--) {
565 this._highlight(children[i], prefix);
566 };
567 };
568 },
569
570
571 /**
572 * Remove highlight of the menu item
573 */
574 lowlight : function () {
575 var e = this.element();
576
577 var marks = e.getElementsByTagName("mark");
578 for (var i = marks.length - 1; i >= 0; i--) {
579 // Create text node clone
580 var x = document.createTextNode(
581 marks[i].firstChild.nodeValue
582 );
583
584 // Replace with content
585 marks[i].parentNode.replaceChild(
586 x,
587 marks[i]
588 );
589 };
590
591 // Remove consecutive textnodes
592 e.normalize();
593 },
594
595 // Initialize menu item
596 _init : function (params) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000597
Nils Diewaldfda29d92015-01-22 17:28:01 +0000598 if (params[0] === undefined)
599 throw new Error("Missing parameters");
600
601 this.content(params[0]);
602
603 if (params.length === 2)
604 this._action = params[1];
605
606 this._lcField = ' ' + this.content().textContent.toLowerCase();
607
608 return this;
609 },
610 };
611
Nils Diewald2fe12e12015-03-06 16:47:06 +0000612/*
613 KorAP._updateKey : function (e) {
614 var code = this._codeFromEvent(e)
615 };
616*/
617
Nils Diewaldfda29d92015-01-22 17:28:01 +0000618}(this.KorAP));