blob: b2510005733ef7e0d497bf9bba692840f5b67c15 [file] [log] [blame]
Nils Diewaldfda29d92015-01-22 17:28:01 +00001var KorAP = KorAP || {};
2
3(function (KorAP) {
4 "use strict";
5
Nils Diewald86dad5b2015-01-28 15:09:07 +00006 // Default maximum number of menu items
7 KorAP.menuLimit = 8;
8
9 /**
10 * List of items for drop down menu (complete).
11 * Only a sublist of the menu is filtered (live).
12 * Only a sublist of the filtered menu is visible (shown).
13 */
14 KorAP.Menu = {
15 /**
16 * Create new Menu based on the action prefix
17 * and a list of menu items.
18 *
19 * @this {Menu}
20 * @constructor
21 * @param {string} Context prefix
22 * @param {Array.<Array.<string>>} List of menu items
23 */
24 create : function (params) {
25 return Object.create(KorAP.Menu)._init(params);
26 },
27
28 _init : function (itemClass, params) {
29 // this._element.addEventListener("click", chooseHint, false);
30 this._itemClass = itemClass;
31 this._element = document.createElement("ul");
32 this._element.style.opacity = 0;
33
34 this.active = false;
35 this._items = new Array();
36 var i;
37 for (i in params) {
38 var obj = itemClass.create(params[i]);
39 this._items.push(
40 obj
41 );
42 };
43 this._limit = KorAP.menuLimit;
44 this._position = 0; // position in the active list
45 this._active = -1; // active item in the item list
46
47 this._reset();
48 return this;
49 },
50
51 element : function () {
52 return this._element;
53 },
54
55 itemClass : function () {
56 return this._itemClass;
57 },
58
59 /**
60 * Get and set numerical value for limit
61 */
62 limit : function (limit) {
63 if (arguments.length === 1)
64 this._limit = limit;
65 return this._limit;
66 },
67
68 /**
69 * Upgrade this object to another object,
70 * while private data stays intact.
71 *
72 * @param {Object] An object with properties.
73 */
74 upgradeTo : function (props) {
75 for (var prop in props) {
76 this[prop] = props[prop];
77 };
78 return this;
79 },
80
81 _reset : function () {
82 this._offset = 0;
83 this._pos = 0;
84 this._prefix = undefined;
85 },
86
87 /**
88 * Filter the list and make it visible
89 *
90 * @param {string} Prefix for filtering the list
91 */
92 show : function (prefix) {
93 this._prefix = prefix;
94
95 // Initialize the list
96 if (!this._initList())
97 return;
98
99 // show based on offset
100 this._showItems(0);
101
102 // Set the first element to active
103 this.liveItem(0).active(true);
104
105 this._position = 0;
106 this._active = this._list[0];
107
108 // Add classes for rolling menus
109 this._boundary(true);
110 },
111
112 /**
113 * Get a specific item from the complete list
114 *
115 * @param {number} index of the list item
116 */
117 item : function (index) {
118 return this._items[index]
119 },
120
121 _initList : function () {
122
123 if (this._list === undefined) {
124 this._list = [];
125 }
126 else if (this._list.length != 0) {
127 this._boundary(false);
128 this._list.length = 0;
129 };
130
131 // Offset is initially zero
132 this._offset = 0;
133
134 if (this.prefix().length <= 0) {
135 for (var i = 0; i < this._items.length; i++)
136 this._list.push(i);
137 return true;
138 };
139
140 var pos;
141 var paddedPrefix = " " + this.prefix();
142
143 for (pos = 0; pos < this._items.length; pos++) {
144 if ((this.item(pos).lcField().indexOf(paddedPrefix)) >= 0)
145 this._list.push(pos);
146 };
147
148 if (this._list.length == 0) {
149 for (pos = 0; pos < this._items.length; pos++) {
150 if ((this.item(pos).lcField().indexOf(this.prefix())) >= 0)
151 this._list.push(pos);
152 };
153 };
154
155 // Filter was successful
156 return this._list.length > 0 ? true : false;
157 },
158
159 // Set boundary for viewport
160 _boundary : function (bool) {
161 this.item(this._list[0]).noMore(bool);
162 this.item(this._list[this._list.length - 1]).noMore(bool);
163 },
164
165 /**
166 * Get the prefix for filtering,
167 * e.g. &quot;ve"&quot; for &quot;verb&quot;
168 */
169 prefix : function () {
170 return this._prefix || '';
171 },
172
173 _showItems : function (offset) {
174 this.delete();
175
176 // Use list
177 var shown = 0;
178 var i;
179 for (i in this._list) {
180
181 // Don't show - it's before offset
182 if (shown++ < offset)
183 continue;
184
185 this._append(this._list[i]);
186
187 if (shown >= (this.limit() + this._offset))
188 break;
189 };
190 },
191
192 /**
193 * Delete all visible items from the menu element
194 */
195 delete : function () {
196 var child;
197 for (var i = 0; i <= this.limit(); i++) {
198
199 if (child = this.shownItem(i))
200 child.lowlight();
201 };
202
203 while (child = this._element.firstChild)
204 this._element.removeChild(child);
205 },
206
207
208 // Append item to the shown list based on index
209 _append : function (i) {
210 var item = this.item(i);
211
212 // Highlight based on prefix
213 if (this.prefix().length > 0)
214 item.highlight(this.prefix());
215
216 // Append element
217 this.element().appendChild(item.element());
218 },
219
220
221 /**
222 * Get a specific item from the filtered list
223 *
224 * @param {number} index of the list item
225 */
226 liveItem : function (index) {
227 if (this._list === undefined)
228 if (!this._initList())
229 return;
230
231 return this._items[this._list[index]];
232 },
233
234 length : function () {
235 return this._items.length;
236 },
237
238
239 /**
240 * Get a specific item from the visible list
241 *
242 * @param {number} index of the list item
243 */
244 shownItem : function (index) {
245 if (index >= this.limit())
246 return;
247 return this.liveItem(this._offset + index);
248 },
249
250
251 /*
252 * Make the next item in the menu active
253 */
254 next : function () {
255 // No active element set
256 if (this._position == -1)
257 return;
258
259 // Set new live item
260 var oldItem = this.liveItem(this._position++);
261 oldItem.active(false);
262 var newItem = this.liveItem(this._position);
263
264 // The next element is undefined - roll to top
265 if (newItem === undefined) {
266 this._offset = 0;
267 this._position = 0;
268 newItem = this.liveItem(0);
269 this._showItems(0);
270 }
271
272 // The next element is outside the view - roll down
273 else if (this._position >= (this.limit + this._offset)) {
274 this._removeFirst();
275 this._offset++;
276 this._append(this._list[this._position]);
277 };
278 newItem.active(true);
279 },
280
281
282 /*
283 * Make the previous item in the menu active
284 */
285/*
286 prev : function () {
287 if (this._position == -1)
288 return;
289
290 // Set new live item
291 var oldItem = this.liveItem(this._position--);
292 oldItem.active(false);
293 var newItem = this.liveItem(this._position);
294
295 // The previous element is undefined - roll to bottom
296 if (newItem === undefined) {
297 this._position = this.liveLength - 1;
298 newItem = this.liveItem(this._position);
299 this._offset = this.liveLength - this.limit;
300 this._showItems(this._offset);
301 }
302
303 // The previous element is outside the view - roll up
304 else if (this._position < this._offset) {
305 this._removeLast();
306 this._offset--;
307 this._prepend(this._list[this._position]);
308 };
309 newItem.active(true);
310 },
311*/
312
313
314 /**
315 * Get the context of the menue,
316 * e.g. &quot;tt/&quot; for the tree tagger menu
317 */
318/*
319 get context () {
320 return this._context;
321 },
322*/
323/*
324 get liveLength () {
325 if (this._list === undefined)
326 this._initList();
327 return this._list.length;
328 },
329*/
330/*
331 chooseHint : function (e) {
332 var element = e.target;
333 while (element.nodeName == "STRONG" || element.nodeName == "SPAN")
334 element = element.parentNode;
335
336 if (element === undefined || element.nodeName != "LI")
337 return;
338
339 var action = element.getAttribute('data-action');
340 hint.insertText(action);
341 var menu = hint.menu();
342 menu.hide();
343
344 // Fill this with the correct value
345 var show;
346 if ((show = hint.analyzeContext()) != "-") {
347 menu.show(show);
348 menu.update(
349 hint._search.getBoundingClientRect().right
350 );
351 };
352
353 hint._search.focus();
354 },
355
356 _removeFirst : function () {
357 this.item(this._list[this._offset]).lowlight();
358 this._element.removeChild(this._element.firstChild);
359 },
360
361 _removeLast : function () {
362 this.item(this._list[this._offset + this.limit - 1]).lowlight();
363 this._element.removeChild(this._element.lastChild);
364 },
365
366
367 // Prepend item to the shown list based on index
368 _prepend : function (i) {
369 var item = this.item(i);
370
371 // Highlight based on prefix
372 if (this.prefix.length > 0)
373 item.highlight(this.prefix);
374
375 // Append element
376 this.element.insertBefore(
377 item.element,
378 this.element.firstChild
379 );
380 },
381*/
382
383 };
384
385
386
387
Nils Diewaldfda29d92015-01-22 17:28:01 +0000388 /**
389 * Item in the Dropdown menu
390 */
391 KorAP.MenuItem = {
392
393 /**
394 * Create a new MenuItem object.
395 *
396 * @constructor
397 * @this {MenuItem}
398 * @param {Array.<string>} An array object of name, action and
399 * optionally a description
400 */
401 create : function (params) {
402 return Object.create(KorAP.MenuItem)._init(params);
403 },
404
405 /**
406 * Upgrade this object to another object,
407 * while private data stays intact.
408 *
409 * @param {Object] An object with properties.
410 */
411 upgradeTo : function (props) {
412 for (var prop in props) {
413 this[prop] = props[prop];
414 };
415 return this;
416 },
417
418 content : function (content) {
419 if (arguments.length === 1)
420 this._content = document.createTextNode(content);
421 return this._content;
422 },
423
424 lcField : function () {
425 return this._lcField;
426 },
427
428 action : function (action) {
429 if (arguments.length === 1)
430 this._action = action;
431 return this._action;
432 },
433
434 /**
435 * Check or set if the item is active
436 *
437 * @param {boolean|null} State of activity
438 */
439 active : function (bool) {
440 var cl = this.element().classList;
441 if (bool === undefined)
442 return cl.contains("active");
443 else if (bool)
444 cl.add("active");
445 else
446 cl.remove("active");
447 },
448
449 /**
450 * Check or set if the item is
451 * at the boundary of the menu
452 * list
453 *
454 * @param {boolean|null} State of activity
455 */
456 noMore : function (bool) {
457 var cl = this.element().classList;
458 if (bool === undefined)
459 return cl.contains("no-more");
460 else if (bool)
461 cl.add("no-more");
462 else
463 cl.remove("no-more");
464 },
465
466 /**
467 * Get the document element of the menu item
468 */
469 element : function () {
470 // already defined
471 if (this._element !== undefined)
472 return this._element;
473
474 // Create list item
475 var li = document.createElement("li");
476
477 // Connect action
478 li["action"] = this._action;
479
480 // Append template
481 li.appendChild(this.content());
482
483 return this._element = li;
484 },
485
486 /**
487 * Highlight parts of the item
488 *
489 * @param {string} Prefix string for highlights
490 */
491 highlight : function (prefix) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000492 var children = this.element().childNodes;
493 for (var i = children.length -1; i >= 0; i--) {
494 this._highlight(children[i], prefix);
495 };
Nils Diewaldfda29d92015-01-22 17:28:01 +0000496 },
497
498 // Highlight a certain substring of the menu item
499 _highlight : function (elem, prefix) {
500
501 if (elem.nodeType === 3) {
502
503 var text = elem.nodeValue;
504 var textlc = text.toLowerCase();
505 var pos = textlc.indexOf(prefix);
506 if (pos >= 0) {
507
508 // First element
509 if (pos > 0) {
510 elem.parentNode.insertBefore(
511 document.createTextNode(text.substr(0, pos)),
512 elem
513 );
514 };
515
516 // Second element
517 var hl = document.createElement("mark");
518 hl.appendChild(
519 document.createTextNode(text.substr(pos, prefix.length))
520 );
521 elem.parentNode.insertBefore(hl, elem);
522
523 // Third element
524 var third = text.substr(pos + prefix.length);
525 if (third.length > 0) {
526 var thirdE = document.createTextNode(third);
527 elem.parentNode.insertBefore(
528 thirdE,
529 elem
530 );
531 this._highlight(thirdE, prefix);
532 };
533
534 var p = elem.parentNode;
535 p.removeChild(elem);
536 };
537 }
538 else {
539 var children = elem.childNodes;
540 for (var i = children.length -1; i >= 0; i--) {
541 this._highlight(children[i], prefix);
542 };
543 };
544 },
545
546
547 /**
548 * Remove highlight of the menu item
549 */
550 lowlight : function () {
551 var e = this.element();
552
553 var marks = e.getElementsByTagName("mark");
554 for (var i = marks.length - 1; i >= 0; i--) {
555 // Create text node clone
556 var x = document.createTextNode(
557 marks[i].firstChild.nodeValue
558 );
559
560 // Replace with content
561 marks[i].parentNode.replaceChild(
562 x,
563 marks[i]
564 );
565 };
566
567 // Remove consecutive textnodes
568 e.normalize();
569 },
570
571 // Initialize menu item
572 _init : function (params) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000573
Nils Diewaldfda29d92015-01-22 17:28:01 +0000574 if (params[0] === undefined)
575 throw new Error("Missing parameters");
576
577 this.content(params[0]);
578
579 if (params.length === 2)
580 this._action = params[1];
581
582 this._lcField = ' ' + this.content().textContent.toLowerCase();
583
584 return this;
585 },
586 };
587
588}(this.KorAP));