blob: 474948a48959b4961670a661692bb7588856a8b2 [file] [log] [blame]
Nils Diewald19ccee92014-12-08 11:30:08 +00001/**
Nils Diewald5c5a7472015-04-02 22:13:38 +00002 * Hint menu for Kalamar.
Nils Diewald19ccee92014-12-08 11:30:08 +00003 *
4 * @author Nils Diewald
5 */
6
Nils Diewald5c5a7472015-04-02 22:13:38 +00007// requires menu.js
Nils Diewald19ccee92014-12-08 11:30:08 +00008
Nils Diewald19ccee92014-12-08 11:30:08 +00009var KorAP = KorAP || {};
10
Nils Diewald19ccee92014-12-08 11:30:08 +000011(function (KorAP) {
12 "use strict";
13
Nils Diewald19ccee92014-12-08 11:30:08 +000014 // Default log message
Nils Diewalddc3862c2014-12-16 02:44:59 +000015 KorAP.log = KorAP.log || function (type, msg) {
Nils Diewald19ccee92014-12-08 11:30:08 +000016 console.log(type + ": " + msg);
17 };
18
Nils Diewald19ccee92014-12-08 11:30:08 +000019 /**
20 * @define {regex} Regular expression for context
21 */
22 KorAP.context =
23 "(?:^|[^-_a-zA-Z0-9])" + // Anchor
24 "((?:[-_a-zA-Z0-9]+?)\/" + // Foundry
25 "(?:" +
26 "(?:[-_a-zA-Z0-9]+?)=" + // Layer
27 "(?:(?:[^:=\/ ]+?):)?" + // Key
28 ")?" +
29 ")$";
30
31 // Initialize hint array
32 KorAP.hintArray = KorAP.hintArray || {};
33
Nils Diewald5c5a7472015-04-02 22:13:38 +000034
35 /**
36 * KorAP.Hint.create({
37 * inputField : node,
38 * context : context regex
39 * });
40 */
41 KorAP.Hint = {
42
43 // Some variables
44 // _firstTry : true,
45 active : false,
46
47 create : function (param) {
48 return Object.create(KorAP.Hint)._init(param);
49 },
50
51 _init : function (param) {
52 param = param || {};
53
54 // Holds all menus per prefix context
55 this._menu = {};
56
57 // Get input field
58 this._inputField = KorAP.InputField.create(
59 param["inputField"] || document.getElementById("q-field")
60 );
61
62 var inputFieldElement = this._inputField.element();
63
64 var that = this;
65
66 // Add event listener for key pressed down
67 inputFieldElement.addEventListener(
68 "keypress", function (e) {
69 var code = _codeFromEvent(e);
70 if (code === 40) {
71 that.show(false);
72 e.halt();
73 };
74 }, false
75 );
76
77 // Move infobox
78 inputFieldElement.addEventListener(
79 "keyup", function (e) {
80 var input = that._inputField;
81 input.update();
Nils Diewald5c5a7472015-04-02 22:13:38 +000082 }
83 );
84
85 // Set Analyzer for context
86 this._analyzer = KorAP.ContextAnalyzer.create(
87 param["context"] || KorAP.context
88 );
Nils Diewald19ccee92014-12-08 11:30:08 +000089 return this;
90 },
91
Nils Diewald5c5a7472015-04-02 22:13:38 +000092 inputField : function () {
93 return this._inputField;
94 },
Nils Diewald19ccee92014-12-08 11:30:08 +000095
Nils Diewald5c5a7472015-04-02 22:13:38 +000096 /**
97 * A new update by keypress
98 */
99 /*
100updateKeyPress : function (e) {
101 if (!this._active)
102 return;
Nils Diewald19ccee92014-12-08 11:30:08 +0000103
Nils Diewald5c5a7472015-04-02 22:13:38 +0000104 var character = String.fromCharCode(_codeFromEvent(e));
105
106 e.halt(); // No event propagation
107
108 // Only relevant for key down
109 console.log("TODO: filter view");
110 },
111 */
112
113 // updateKeyDown : function (e) {},
114
115 /**
116 * Return hint menu and probably init based on an action
117 */
118 menu : function (action) {
119
120 if (this._menu[action] === undefined) {
121
122 // No matching hint menu
123 if (KorAP.hintArray[action] === undefined)
124 return;
125
126 // Create matching hint menu
127 this._menu[action] = KorAP.HintMenu.create(
128 this, action, KorAP.hintArray[action]
Nils Diewald19ccee92014-12-08 11:30:08 +0000129 );
Nils Diewald19ccee92014-12-08 11:30:08 +0000130
Nils Diewald5c5a7472015-04-02 22:13:38 +0000131 };
132
133 // Return matching hint menu
134 return this._menu[action];
135 },
136
137 /**
138 * Get the correct menu based on the context
139 */
140 contextMenu : function (ifContext) {
141 var context = this._inputField.context();
142 if (context === undefined || context.length == 0)
143 return ifContext ? undefined : this.menu("-");
144
145 context = this._analyzer.test(context);
146 if (context === undefined || context.length == 0)
147 return ifContext ? undefined : this.menu("-");
148
149 return this.menu(context);
150 },
151
152
153 /**
154 * Show the menu
155 */
156 show : function (ifContext) {
157
158 // Menu is already active
159 if (this.active)
160 return;
161
162 // Initialize the menus position
163 /*
164 if (this._firstTry) {
165 this._inputField.reposition();
166 this._firstTry = false;
167 };
168 */
169
170 // update
171
172 // Get the menu
173 var menu;
174 if (menu = this.contextMenu(ifContext)) {
Nils Diewald2488d052015-04-09 21:46:02 +0000175 var c = this._inputField.container();
176 c.classList.add('active');
177 c.appendChild(menu.element());
Nils Diewald5c5a7472015-04-02 22:13:38 +0000178 menu.show('');
179 menu.focus();
180// Update bounding box
181/*
182 }
183 else if (!ifContext) {
184 // this.hide();
185 };
186*/
187 // Focus on input field
188 // this.inputField.element.focus();
Nils Diewald19ccee92014-12-08 11:30:08 +0000189 };
190 }
191 };
Nils Diewald5c5a7472015-04-02 22:13:38 +0000192
193
Nils Diewald2488d052015-04-09 21:46:02 +0000194 // Input field for queries
195 KorAP.InputField = {
196 create : function (element) {
197 return Object.create(KorAP.InputField)._init(element);
198 },
199
200 _init : function (element) {
201 this._element = element;
202
203 // Create mirror for searchField
204 if ((this._mirror = document.getElementById("searchMirror")) === null) {
205 this._mirror = document.createElement("div");
206 this._mirror.setAttribute("id", "searchMirror");
207 this._mirror.appendChild(document.createElement("span"));
208 this._container = this._mirror.appendChild(document.createElement("div"));
209 this._mirror.style.height = "0px";
210 document.getElementsByTagName("body")[0].appendChild(this._mirror);
211 };
212
213 // Update position of the mirror
214 var that = this;
215 var repos = function () {
216 that.reposition();
217 };
218 window.addEventListener('resize', repos);
219 this._element.addEventListener('onfocus', repos);
220 that.reposition();
221
222 return this;
223 },
224
225 rightPos : function () {
226 var box = this._mirror.firstChild.getBoundingClientRect();
227 return box.right - box.left;
228 },
229
230 mirror : function () {
231 return this._mirror;
232 },
233
234 container : function () {
235 return this._container;
236 },
237
238 element : function () {
239 return this._element;
240 },
241
242 value : function () {
243 return this._element.value;
244 },
245
246 update : function () {
247 this._mirror.firstChild.textContent = this.split()[0];
248 this._container.style.left = this.rightPos() + 'px';
249 },
250
251 insert : function (text) {
252 var splittedText = this.split();
253 var s = this._element;
254 s.value = splittedText[0] + text + splittedText[1];
255 s.selectionStart = (splittedText[0] + text).length;
256 s.selectionEnd = s.selectionStart;
257 this._mirror.firstChild.textContent = splittedText[0] + text;
258 },
259
260 // Return two substrings, splitted at current cursor position
261 split : function () {
262 var s = this._element;
263 var value = s.value;
264 var start = s.selectionStart;
265 return new Array(
266 value.substring(0, start),
267 value.substring(start, value.length)
268 );
269 },
270
271 // Position the input mirror directly below the input box
272 reposition : function () {
273 var inputClientRect = this._element.getBoundingClientRect();
274 var inputStyle = window.getComputedStyle(this._element, null);
275
276 var bodyClientRect =
277 document.getElementsByTagName('body')[0].getBoundingClientRect();
278
279 // Reset position
280 var mirrorStyle = this._mirror.style;
281 mirrorStyle.left = inputClientRect.left + "px";
282 mirrorStyle.top = (inputClientRect.bottom - bodyClientRect.top) + "px";
283 mirrorStyle.width = inputStyle.getPropertyValue("width");
284
285 // These may be relevant in case of media depending css
286 mirrorStyle.paddingLeft = inputStyle.getPropertyValue("padding-left");
287 mirrorStyle.marginLeft = inputStyle.getPropertyValue("margin-left");
288 mirrorStyle.borderLeftWidth = inputStyle.getPropertyValue("border-left-width");
289 mirrorStyle.borderLeftStyle = inputStyle.getPropertyValue("border-left-style");
290 mirrorStyle.fontSize = inputStyle.getPropertyValue("font-size");
291 mirrorStyle.fontFamily = inputStyle.getPropertyValue("font-family");
292 },
293 context : function () {
294 return this.split()[0];
295 }
296 };
297
298
299 /**
300 * Regex object for checking the context of the hint
301 */
302 KorAP.ContextAnalyzer = {
303 create : function (regex) {
304 return Object.create(KorAP.ContextAnalyzer)._init(regex);
305 },
306 _init : function (regex) {
307 try {
308 this._regex = new RegExp(regex);
309 }
310 catch (e) {
311 KorAP.log(0, e);
312 return;
313 };
314 return this;
315 },
316 test : function (text) {
317 if (!this._regex.exec(text))
318 return;
319 return RegExp.$1;
320 }
321 };
322
323
324 /**
325 * Hint menu
326 */
327 KorAP.HintMenu = {
328 create : function (hint, context, params) {
329 var obj = Object.create(KorAP.Menu)
330 .upgradeTo(KorAP.HintMenu)
331 ._init(KorAP.HintMenuItem, KorAP.HintMenuPrefix, params);
332 obj._context = context;
333 obj._element.classList.add('hint');
334 obj._hint = hint;
335
336 // This is only domspecific
337 obj.element().addEventListener('blur', function (e) {
338 this.menu.hide();
339 });
340
341 // Focus on input field on hide
342 obj.onHide = function () {
343 var input = this._hint.inputField();
344 input.container().classList.remove('active');
345 input.element().focus();
346 };
347
348 return obj;
349 },
350 // Todo: Is this necessary?
351 context : function () {
352 return this._context;
353 },
354 hint : function () {
355 return this._hint;
356 }
357 };
358
359
360 /**
361 * Hint menu item based on MenuItem
362 */
363 KorAP.HintMenuItem = {
364 create : function (params) {
365 return Object.create(KorAP.MenuItem)
366 .upgradeTo(KorAP.HintMenuItem)
367 ._init(params);
368 },
369 content : function (content) {
370 if (arguments.length === 1) {
371 this._content = content;
372 };
373 return this._content;
374 },
375 _init : function (params) {
376 if (params[0] === undefined ||
377 params[1] === undefined)
378 throw new Error("Missing parameters");
379
380 this._name = params[0];
381 this._action = params[1];
382 this._lcField = ' ' + this._name.toLowerCase();
383
384 if (params.length > 2) {
385 this._desc = params[2];
386 this._lcField += " " + this._desc.toLowerCase();
387 };
388
389 return this;
390 },
391 onclick : function () {
392 var m = this.menu();
393 var h = m.hint();
394 m.hide();
395
396 // Update input field
397 var input = h.inputField();
398 input.insert(this._action);
399 input.update();
400
401 h.active = false;
402 h.show(true);
403 },
404 name : function () {
405 return this._name;
406 },
407 action : function () {
408 return this._action;
409 },
410 desc : function () {
411 return this._desc;
412 },
413 element : function () {
414 // already defined
415 if (this._element !== undefined)
416 return this._element;
417
418 // Create list item
419 var li = document.createElement("li");
420
421 if (this.onclick !== undefined) {
422 li["onclick"] = this.onclick.bind(this);
423 };
424
425 // Create title
426 var name = document.createElement("span");
427 name.appendChild(document.createTextNode(this._name));
428
429 li.appendChild(name);
430
431 // Create description
432 if (this._desc !== undefined) {
433 var desc = document.createElement("span");
434 desc.classList.add('desc');
435 desc.appendChild(document.createTextNode(this._desc));
436 li.appendChild(desc);
437 };
438 return this._element = li;
439 }
440 };
441
442 KorAP.HintMenuPrefix = {
443 create : function (params) {
444 return Object.create(KorAP.MenuPrefix).upgradeTo(KorAP.HintMenuPrefix)._init(params);
445 },
446 onclick : function () {
447 var m = this.menu();
448 var h = m.hint();
449 m.hide();
450
451 h.inputField().insert(this.value());
452 h.active = false;
453 }
454 };
455
456
Nils Diewald5c5a7472015-04-02 22:13:38 +0000457 /**
458 * Return keycode based on event
459 */
460 function _codeFromEvent (e) {
461 if ((e.charCode) && (e.keyCode==0))
462 return e.charCode
463 return e.keyCode;
464 };
465
Nils Diewald19ccee92014-12-08 11:30:08 +0000466}(this.KorAP));