blob: 80631ec9a56a0a9a6a9f994f1e7580c3d58632d0 [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 // Input field for queries
Nils Diewald19ccee92014-12-08 11:30:08 +000035 KorAP.InputField = {
36 create : function (element) {
37 return Object.create(KorAP.InputField)._init(element);
38 },
Nils Diewald5c5a7472015-04-02 22:13:38 +000039
Nils Diewald19ccee92014-12-08 11:30:08 +000040 _init : function (element) {
41 this._element = element;
42
43 // Create mirror for searchField
44 if ((this._mirror = document.getElementById("searchMirror")) === null) {
45 this._mirror = document.createElement("div");
46 this._mirror.setAttribute("id", "searchMirror");
47 this._mirror.appendChild(document.createElement("span"));
Nils Diewald5c5a7472015-04-02 22:13:38 +000048 this._container = this._mirror.appendChild(document.createElement("div"));
Nils Diewald19ccee92014-12-08 11:30:08 +000049 this._mirror.style.height = "1px";
50 document.getElementsByTagName("body")[0].appendChild(this._mirror);
51 };
52
53 // Update position of the mirror
54 var that = this;
Nils Diewald58141332015-04-07 16:18:45 +000055 window.onresize = function () {
Nils Diewald19ccee92014-12-08 11:30:08 +000056 that.reposition();
57 };
Nils Diewald5c5a7472015-04-02 22:13:38 +000058
59 that.reposition();
60
Nils Diewald19ccee92014-12-08 11:30:08 +000061 return this;
62 },
Nils Diewald5c5a7472015-04-02 22:13:38 +000063
64 rightPos : function () {
65 var box = this._mirror.getBoundingClientRect();
66 return box.right - box.left;
67 },
68
69 mirror : function () {
Nils Diewald19ccee92014-12-08 11:30:08 +000070 return this._mirror;
71 },
Nils Diewald5c5a7472015-04-02 22:13:38 +000072
73 container : function () {
74 return this._container;
75 },
76
77 element : function () {
Nils Diewald19ccee92014-12-08 11:30:08 +000078 return this._element;
79 },
Nils Diewald5c5a7472015-04-02 22:13:38 +000080
81 value : function () {
Nils Diewald19ccee92014-12-08 11:30:08 +000082 return this._element.value;
83 },
Nils Diewald5c5a7472015-04-02 22:13:38 +000084
Nils Diewald19ccee92014-12-08 11:30:08 +000085 update : function () {
86 this._mirror.firstChild.textContent = this.split()[0];
87 },
Nils Diewald5c5a7472015-04-02 22:13:38 +000088
Nils Diewald19ccee92014-12-08 11:30:08 +000089 insert : function (text) {
90 var splittedText = this.split();
Nils Diewald5c5a7472015-04-02 22:13:38 +000091 var s = this._element;
Nils Diewald19ccee92014-12-08 11:30:08 +000092 s.value = splittedText[0] + text + splittedText[1];
93 s.selectionStart = (splittedText[0] + text).length;
94 s.selectionEnd = s.selectionStart;
95 this._mirror.firstChild.textContent = splittedText[0] + text;
96 },
Nils Diewald5c5a7472015-04-02 22:13:38 +000097
98 // Return two substrings, splitted at current cursor position
Nils Diewald19ccee92014-12-08 11:30:08 +000099 split : function () {
100 var s = this._element;
101 var value = s.value;
102 var start = s.selectionStart;
103 return new Array(
104 value.substring(0, start),
105 value.substring(start, value.length)
106 );
107 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000108
Nils Diewald19ccee92014-12-08 11:30:08 +0000109 // Position the input mirror directly below the input box
110 reposition : function () {
111 var inputClientRect = this._element.getBoundingClientRect();
112 var inputStyle = window.getComputedStyle(this._element, null);
Nils Diewald5c5a7472015-04-02 22:13:38 +0000113
114 // Reset position
Nils Diewald19ccee92014-12-08 11:30:08 +0000115 var mirrorStyle = this._mirror.style;
116 mirrorStyle.left = inputClientRect.left + "px";
Nils Diewald5c5a7472015-04-02 22:13:38 +0000117 mirrorStyle.top = inputClientRect.bottom + "px";
Nils Diewald19ccee92014-12-08 11:30:08 +0000118
119 // These may be relevant in case of media depending css
120 mirrorStyle.paddingLeft = inputStyle.getPropertyValue("padding-left");
121 mirrorStyle.marginLeft = inputStyle.getPropertyValue("margin-left");
122 mirrorStyle.borderLeftWidth = inputStyle.getPropertyValue("border-left-width");
123 mirrorStyle.borderLeftStyle = inputStyle.getPropertyValue("border-left-style");
124 mirrorStyle.fontSize = inputStyle.getPropertyValue("font-size");
125 mirrorStyle.fontFamily = inputStyle.getPropertyValue("font-family");
126 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000127 context : function () {
Nils Diewald19ccee92014-12-08 11:30:08 +0000128 return this.split()[0];
129 }
130 };
131
Nils Diewald19ccee92014-12-08 11:30:08 +0000132
133 /**
Nils Diewald5c5a7472015-04-02 22:13:38 +0000134 * Regex object for checking the context of the hint
Nils Diewald19ccee92014-12-08 11:30:08 +0000135 */
136 KorAP.ContextAnalyzer = {
137 create : function (regex) {
138 return Object.create(KorAP.ContextAnalyzer)._init(regex);
139 },
140 _init : function (regex) {
141 try {
142 this._regex = new RegExp(regex);
143 }
144 catch (e) {
145 KorAP.log("error", e);
146 return;
147 };
148 return this;
149 },
150 test : function (text) {
151 if (!this._regex.exec(text))
152 return;
153 return RegExp.$1;
154 }
155 };
156
157
158 /**
Nils Diewald5c5a7472015-04-02 22:13:38 +0000159 * Hint menu item based on MenuItem
Nils Diewald19ccee92014-12-08 11:30:08 +0000160 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000161 KorAP.HintMenuItem = {
162 create : function (params) {
163 return Object.create(KorAP.MenuItem)
164 .upgradeTo(KorAP.HintMenuItem)
165 ._init(params);
Nils Diewald19ccee92014-12-08 11:30:08 +0000166 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000167 content : function (content) {
168 if (arguments.length === 1) {
169 this._content = content;
Nils Diewald19ccee92014-12-08 11:30:08 +0000170 };
Nils Diewald5c5a7472015-04-02 22:13:38 +0000171 return this._content;
Nils Diewald19ccee92014-12-08 11:30:08 +0000172 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000173 _init : function (params) {
174 if (params[0] === undefined ||
175 params[1] === undefined)
176 throw new Error("Missing parameters");
177
178 this._name = params[0];
179 this._action = params[1];
180 this._lcField = ' ' + this._name.toLowerCase();
181
182 if (params.length > 2) {
183 this._desc = params[2];
184 this._lcField += " " + this._desc.toLowerCase();
Nils Diewald19ccee92014-12-08 11:30:08 +0000185 };
186
Nils Diewald19ccee92014-12-08 11:30:08 +0000187 return this;
188 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000189 onclick : function () {
190 var m = this.menu();
191 var h = m.hint();
192 m.hide();
Nils Diewald19ccee92014-12-08 11:30:08 +0000193
Nils Diewald5c5a7472015-04-02 22:13:38 +0000194 h.inputField().insert(this._action);
195 h.active = false;
Nils Diewald19ccee92014-12-08 11:30:08 +0000196
Nils Diewald5c5a7472015-04-02 22:13:38 +0000197 h.show(true);
Nils Diewald19ccee92014-12-08 11:30:08 +0000198 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000199 name : function () {
Nils Diewald19ccee92014-12-08 11:30:08 +0000200 return this._name;
201 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000202 action : function () {
Nils Diewald19ccee92014-12-08 11:30:08 +0000203 return this._action;
204 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000205 desc : function () {
Nils Diewald19ccee92014-12-08 11:30:08 +0000206 return this._desc;
207 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000208 element : function () {
Nils Diewald19ccee92014-12-08 11:30:08 +0000209 // already defined
210 if (this._element !== undefined)
211 return this._element;
212
213 // Create list item
214 var li = document.createElement("li");
Nils Diewald5c5a7472015-04-02 22:13:38 +0000215
216 if (this.onclick !== undefined) {
217 li["onclick"] = this.onclick.bind(this);
218 };
Nils Diewald19ccee92014-12-08 11:30:08 +0000219
220 // Create title
Nils Diewald5c5a7472015-04-02 22:13:38 +0000221 var name = document.createElement("span");
Nils Diewald19ccee92014-12-08 11:30:08 +0000222 name.appendChild(document.createTextNode(this._name));
Nils Diewald5c5a7472015-04-02 22:13:38 +0000223
Nils Diewald19ccee92014-12-08 11:30:08 +0000224 li.appendChild(name);
225
226 // Create description
227 if (this._desc !== undefined) {
228 var desc = document.createElement("span");
Nils Diewald5c5a7472015-04-02 22:13:38 +0000229 desc.classList.add('desc');
Nils Diewald19ccee92014-12-08 11:30:08 +0000230 desc.appendChild(document.createTextNode(this._desc));
231 li.appendChild(desc);
232 };
233 return this._element = li;
Nils Diewald5c5a7472015-04-02 22:13:38 +0000234 }
235 };
236
237 KorAP.HintMenuPrefix = {
238 create : function (params) {
239 return Object.create(KorAP.MenuPrefix).upgradeTo(KorAP.HintMenuPrefix)._init(params);
Nils Diewald19ccee92014-12-08 11:30:08 +0000240 },
Nils Diewald5c5a7472015-04-02 22:13:38 +0000241 onclick : function () {
242 var m = this.menu();
243 var h = m.hint();
244 m.hide();
Nils Diewald19ccee92014-12-08 11:30:08 +0000245
Nils Diewald5c5a7472015-04-02 22:13:38 +0000246 h.inputField().insert(this.value());
247 h.active = false;
248 }
249 };
Nils Diewald19ccee92014-12-08 11:30:08 +0000250
Nils Diewald5c5a7472015-04-02 22:13:38 +0000251 KorAP.HintMenu = {
252 create : function (hint, context, params) {
253 var obj = Object.create(KorAP.Menu)
254 .upgradeTo(KorAP.HintMenu)
255 ._init(KorAP.HintMenuItem, KorAP.HintMenuPrefix, params);
256 obj._context = context;
257 obj._element.classList.add('hint');
258 obj._hint = hint;
Nils Diewald19ccee92014-12-08 11:30:08 +0000259
Nils Diewald5c5a7472015-04-02 22:13:38 +0000260 // This is only domspecific
261 obj.element().addEventListener('blur', function (e) {
262 this.menu.hide();
263 });
Nils Diewald19ccee92014-12-08 11:30:08 +0000264
Nils Diewald5c5a7472015-04-02 22:13:38 +0000265 // Focus on input field on hide
266 obj.onHide = function () {
267 var input = this._hint.inputField();
268 input.element().focus();
Nils Diewald19ccee92014-12-08 11:30:08 +0000269 };
Nils Diewald5c5a7472015-04-02 22:13:38 +0000270
271 return obj;
272 },
273 // Todo: Is this necessary?
274 context : function () {
275 return this._context;
276 },
277 hint : function () {
278 return this._hint;
279 }
280 };
281
282
283 /**
284 * KorAP.Hint.create({
285 * inputField : node,
286 * context : context regex
287 * });
288 */
289 KorAP.Hint = {
290
291 // Some variables
292 // _firstTry : true,
293 active : false,
294
295 create : function (param) {
296 return Object.create(KorAP.Hint)._init(param);
297 },
298
299 _init : function (param) {
300 param = param || {};
301
302 // Holds all menus per prefix context
303 this._menu = {};
304
305 // Get input field
306 this._inputField = KorAP.InputField.create(
307 param["inputField"] || document.getElementById("q-field")
308 );
309
310 var inputFieldElement = this._inputField.element();
311
312 var that = this;
313
Nils Diewald58141332015-04-07 16:18:45 +0000314
Nils Diewald5c5a7472015-04-02 22:13:38 +0000315 // Add event listener for key pressed down
316 inputFieldElement.addEventListener(
317 "keypress", function (e) {
318 var code = _codeFromEvent(e);
319 if (code === 40) {
320 that.show(false);
321 e.halt();
322 };
323 }, false
324 );
325
326 // Move infobox
327 inputFieldElement.addEventListener(
328 "keyup", function (e) {
329 var input = that._inputField;
330 input.update();
331 input.container().style.left = input.rightPos() + 'px';
332 }
333 );
334
335 // Set Analyzer for context
336 this._analyzer = KorAP.ContextAnalyzer.create(
337 param["context"] || KorAP.context
338 );
Nils Diewald19ccee92014-12-08 11:30:08 +0000339 return this;
340 },
341
Nils Diewald5c5a7472015-04-02 22:13:38 +0000342 inputField : function () {
343 return this._inputField;
344 },
Nils Diewald19ccee92014-12-08 11:30:08 +0000345
Nils Diewald5c5a7472015-04-02 22:13:38 +0000346 /**
347 * A new update by keypress
348 */
349 /*
350updateKeyPress : function (e) {
351 if (!this._active)
352 return;
Nils Diewald19ccee92014-12-08 11:30:08 +0000353
Nils Diewald5c5a7472015-04-02 22:13:38 +0000354 var character = String.fromCharCode(_codeFromEvent(e));
355
356 e.halt(); // No event propagation
357
358 // Only relevant for key down
359 console.log("TODO: filter view");
360 },
361 */
362
363 // updateKeyDown : function (e) {},
364
365 /**
366 * Return hint menu and probably init based on an action
367 */
368 menu : function (action) {
369
370 if (this._menu[action] === undefined) {
371
372 // No matching hint menu
373 if (KorAP.hintArray[action] === undefined)
374 return;
375
376 // Create matching hint menu
377 this._menu[action] = KorAP.HintMenu.create(
378 this, action, KorAP.hintArray[action]
Nils Diewald19ccee92014-12-08 11:30:08 +0000379 );
Nils Diewald19ccee92014-12-08 11:30:08 +0000380
Nils Diewald5c5a7472015-04-02 22:13:38 +0000381 };
382
383 // Return matching hint menu
384 return this._menu[action];
385 },
386
387 /**
388 * Get the correct menu based on the context
389 */
390 contextMenu : function (ifContext) {
391 var context = this._inputField.context();
392 if (context === undefined || context.length == 0)
393 return ifContext ? undefined : this.menu("-");
394
395 context = this._analyzer.test(context);
396 if (context === undefined || context.length == 0)
397 return ifContext ? undefined : this.menu("-");
398
399 return this.menu(context);
400 },
401
402
403 /**
404 * Show the menu
405 */
406 show : function (ifContext) {
407
408 // Menu is already active
409 if (this.active)
410 return;
411
412 // Initialize the menus position
413 /*
414 if (this._firstTry) {
415 this._inputField.reposition();
416 this._firstTry = false;
417 };
418 */
419
420 // update
421
422 // Get the menu
423 var menu;
424 if (menu = this.contextMenu(ifContext)) {
425 this._inputField.container().appendChild(menu.element());
426 menu.show('');
427 menu.focus();
428// Update bounding box
429/*
430 }
431 else if (!ifContext) {
432 // this.hide();
433 };
434*/
435 // Focus on input field
436 // this.inputField.element.focus();
Nils Diewald19ccee92014-12-08 11:30:08 +0000437 };
438 }
439 };
Nils Diewald5c5a7472015-04-02 22:13:38 +0000440
441
442 /**
443 * Return keycode based on event
444 */
445 function _codeFromEvent (e) {
446 if ((e.charCode) && (e.keyCode==0))
447 return e.charCode
448 return e.keyCode;
449 };
450
Nils Diewald19ccee92014-12-08 11:30:08 +0000451}(this.KorAP));