blob: bcdd6a6f068c15edf72f00a6fbfd9ec079d2f4de [file] [log] [blame]
Nils Diewald19ccee92014-12-08 11:30:08 +00001/**
Nils Diewald5c5a7472015-04-02 22:13:38 +00002 * Hint menu for Kalamar.
Nils Diewald47f366b2015-04-15 20:06:35 +00003 * Based on menu object.
Nils Diewald19ccee92014-12-08 11:30:08 +00004 *
5 * @author Nils Diewald
6 */
Akron308db382016-05-30 22:34:07 +02007/*
Akron65c74352016-09-02 17:23:39 +02008 * TODO: Check for cnx/syn=
Akron308db382016-05-30 22:34:07 +02009 * TODO: List can be shown when prefix is like 'base/s=pcorenlp/'
10 * TODO: Sometimes the drop-down box down vanish when list is shown
11 * TODO: Create should expect an input text field
12 * TODO: Embed only one single menu (not multiple)
Akron02360e42016-06-07 13:41:12 +020013 * By holding the current menu in _active
14 * TODO: show() should accept a context field (especially for no-context fields,
15 * like fragments)
16 * TODO: Improve context analyzer from hint!
17 * TODO: Marked annotations should be addable to "fragments"
Akron308db382016-05-30 22:34:07 +020018 */
Akronda5bd3a2020-10-16 17:37:49 +020019
20"use strict";
21
Nils Diewald0e6992a2015-04-14 20:13:52 +000022define([
23 'hint/input',
24 'hint/menu',
25 'hint/contextanalyzer',
Akron00cd4d12016-05-31 21:01:11 +020026 'hint/alert',
Nils Diewald0e6992a2015-04-14 20:13:52 +000027 'util'
28], function (inputClass,
Akronc14cbfc2018-08-31 13:15:55 +020029 menuClass,
30 analyzerClass,
31 alertClass) {
Nils Diewald0e6992a2015-04-14 20:13:52 +000032
33 // Initialize hint array
Nils Diewald5c5a7472015-04-02 22:13:38 +000034
35 /**
36 * KorAP.Hint.create({
37 * inputField : node,
38 * context : context regex
39 * });
40 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000041 return {
Nils Diewald5c5a7472015-04-02 22:13:38 +000042
Nils Diewald0e6992a2015-04-14 20:13:52 +000043 /**
44 * Create new hint helper.
45 */
Nils Diewald5c5a7472015-04-02 22:13:38 +000046 create : function (param) {
Nils Diewald0e6992a2015-04-14 20:13:52 +000047 return Object.create(this)._init(param);
Nils Diewald5c5a7472015-04-02 22:13:38 +000048 },
49
Akronda5bd3a2020-10-16 17:37:49 +020050
Nils Diewald0e6992a2015-04-14 20:13:52 +000051 // Initialize hint helper
Nils Diewald5c5a7472015-04-02 22:13:38 +000052 _init : function (param) {
53 param = param || {};
54
55 // Holds all menus per prefix context
Leo Repp9b26ba92021-09-17 17:38:22 +020056 this._menuCollection = {};
Akron00cd4d12016-05-31 21:01:11 +020057 this._alert = alertClass.create();
Akronda5bd3a2020-10-16 17:37:49 +020058
59 // Active may either hold an alert or a menu
Akron02360e42016-06-07 13:41:12 +020060 this._active = null;
Nils Diewald5c5a7472015-04-02 22:13:38 +000061
Akron80055992017-12-20 16:30:52 +010062 // No annotation helper available
63 if (!KorAP.annotationHelper) {
64 console.log("No annotationhelper defined");
65 return;
66 };
Marc Kupietzaa6709c2025-12-19 20:03:54 +010067
68 // Apply configured foundry filter from data-hint-foundries attribute
69 if (KorAP.annotationHelper.filterByConfig) {
70 KorAP.annotationHelper.filterByConfig();
71 };
Akronfac16472018-07-26 16:47:21 +020072
73 /**
74 * @define {regex} Regular expression for context
75 */
76 KorAP.context = KorAP.context ||
77 "(?:^|[^-_a-zA-Z0-9])" + // Anchor
78 "((?:[-_a-zA-Z0-9]+?)\/" + // Foundry
79 "(?:" +
80 "(?:[-_a-zA-Z0-9]+?)=" + // Layer
81 "(?:"+
82 "(?:[^:=\/ ]+?):|" + // Key
83 "(?:[^-=\/ ]+?)-" + // Node
84 ")?" +
85 ")?" +
86 ")$";
Akron80055992017-12-20 16:30:52 +010087
Nils Diewald5c5a7472015-04-02 22:13:38 +000088 // Get input field
Akronda5bd3a2020-10-16 17:37:49 +020089 const qfield = param["inputField"] || document.getElementById("q-field");
Nils Diewald0e6992a2015-04-14 20:13:52 +000090 if (!qfield)
Akron8eaeb2e2016-08-29 18:26:28 +020091 return null;
Nils Diewald0e6992a2015-04-14 20:13:52 +000092
Akron00cd4d12016-05-31 21:01:11 +020093 // Create input field
Nils Diewald0e6992a2015-04-14 20:13:52 +000094 this._inputField = inputClass.create(qfield);
Nils Diewald5c5a7472015-04-02 22:13:38 +000095
Akron00cd4d12016-05-31 21:01:11 +020096 // create alert
Akronda5bd3a2020-10-16 17:37:49 +020097 const that = this;
98
99 const c = this._inputField.container();
Akron00cd4d12016-05-31 21:01:11 +0200100 c.appendChild(this._alert.element());
Akronda5bd3a2020-10-16 17:37:49 +0200101 c.addEventListener('click', function (e) {
Akronc14cbfc2018-08-31 13:15:55 +0200102 if (!this.classList.contains('active')) {
103 that.show(false);
104 };
Akrondadd1d12021-11-12 16:20:28 +0100105 e.halt();
Nils Diewald47f366b2015-04-15 20:06:35 +0000106 });
107
Nils Diewald47f366b2015-04-15 20:06:35 +0000108 // Move infobox
Akronda5bd3a2020-10-16 17:37:49 +0200109 const inputFieldElement = this._inputField.element();
Akron00cd4d12016-05-31 21:01:11 +0200110 inputFieldElement.addEventListener("keyup", this.update.bind(this));
111 inputFieldElement.addEventListener("click", this.update.bind(this));
112
113 // Add event listener for key pressed down
Akronda5bd3a2020-10-16 17:37:49 +0200114 let _down = function (e) {
115 if (_codeFromEvent(e) === 40) {
116 this.show(false);
117 e.halt();
118 };
119 };
120
Akron00cd4d12016-05-31 21:01:11 +0200121 inputFieldElement.addEventListener(
Akronc14cbfc2018-08-31 13:15:55 +0200122 "keydown", _down.bind(this), false
123 );
124
125 // Add touch events
126 inputFieldElement.addEventListener(
127 'touchstart',
128 this._touch.bind(this),
129 false
130 );
131 inputFieldElement.addEventListener(
132 'touchend',
133 this._touch.bind(this),
134 false
135 );
136 inputFieldElement.addEventListener(
137 'touchmove',
138 this._touch.bind(this),
139 false
Akron00cd4d12016-05-31 21:01:11 +0200140 );
Nils Diewald5c5a7472015-04-02 22:13:38 +0000141
142 // Set Analyzer for context
Nils Diewald0e6992a2015-04-14 20:13:52 +0000143 this._analyzer = analyzerClass.create(
Akronc14cbfc2018-08-31 13:15:55 +0200144 param["context"] || KorAP.context
Nils Diewald5c5a7472015-04-02 22:13:38 +0000145 );
Akron1c18f102024-11-19 16:31:06 +0100146
147 this.update();
Nils Diewald19ccee92014-12-08 11:30:08 +0000148 return this;
149 },
150
Akron308db382016-05-30 22:34:07 +0200151
152 /**
153 * Return the input field attached to the hint helper.
154 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000155 inputField : function () {
156 return this._inputField;
157 },
Nils Diewald19ccee92014-12-08 11:30:08 +0000158
Akron308db382016-05-30 22:34:07 +0200159
160 /**
Akronb759ee92024-11-19 18:02:56 +0100161 * Return selection range of the input field.
162 */
163 selectionRange : function () {
164 return this._inputField.selectionRange();
165 },
166
167
168 /**
Akrondadd1d12021-11-12 16:20:28 +0100169 * Alert at a specific character position.
Akron308db382016-05-30 22:34:07 +0200170 */
Akron00cd4d12016-05-31 21:01:11 +0200171 alert : function (charPos, msg) {
Akronda5bd3a2020-10-16 17:37:49 +0200172 const t = this;
Akron00cd4d12016-05-31 21:01:11 +0200173 if (arguments.length === 0)
Akronda5bd3a2020-10-16 17:37:49 +0200174 return t._alert;
Akron00cd4d12016-05-31 21:01:11 +0200175
176 // Do not alert if already alerted!
Akronda5bd3a2020-10-16 17:37:49 +0200177 if (t._alert.active)
Akronc14cbfc2018-08-31 13:15:55 +0200178 return false;
Akron00cd4d12016-05-31 21:01:11 +0200179
180 // Move to the correct position
Akronda5bd3a2020-10-16 17:37:49 +0200181 t._inputField.moveto(charPos);
Akron00cd4d12016-05-31 21:01:11 +0200182
183 // Set container to active (aka hide the hint helper button)
184
Akronda5bd3a2020-10-16 17:37:49 +0200185 t._alert.show(msg);
186 t.active(t._alert);
Akron00cd4d12016-05-31 21:01:11 +0200187 return true;
Akron308db382016-05-30 22:34:07 +0200188 },
Akron8eaeb2e2016-08-29 18:26:28 +0200189
Akron00cd4d12016-05-31 21:01:11 +0200190
Akronda5bd3a2020-10-16 17:37:49 +0200191 /**
192 * Update input field.
193 */
Akron00cd4d12016-05-31 21:01:11 +0200194 update : function () {
195 this._inputField.update();
Akron02360e42016-06-07 13:41:12 +0200196 if (this._alert.hide())
Akronc14cbfc2018-08-31 13:15:55 +0200197 this.active(null);
Akron00cd4d12016-05-31 21:01:11 +0200198 },
199
200
Nils Diewald5c5a7472015-04-02 22:13:38 +0000201 /**
Nils Diewald5c5a7472015-04-02 22:13:38 +0000202 * Return hint menu and probably init based on an action
203 */
204 menu : function (action) {
Akrondadd1d12021-11-12 16:20:28 +0100205
Leo Repp9b26ba92021-09-17 17:38:22 +0200206 if (this._menuCollection[action] === undefined) {
Akronda5bd3a2020-10-16 17:37:49 +0200207
Akronc14cbfc2018-08-31 13:15:55 +0200208 // No matching hint menu
209 if (KorAP.annotationHelper[action] === undefined)
210 return;
Nils Diewald5c5a7472015-04-02 22:13:38 +0000211
Akronc14cbfc2018-08-31 13:15:55 +0200212 // Create matching hint menu
Leo Repp9b26ba92021-09-17 17:38:22 +0200213 this._menuCollection[action] = menuClass.create(
Akronc14cbfc2018-08-31 13:15:55 +0200214 this, action, KorAP.annotationHelper[action]
215 );
Nils Diewald5c5a7472015-04-02 22:13:38 +0000216 };
217
218 // Return matching hint menu
Leo Repp9b26ba92021-09-17 17:38:22 +0200219 return this._menuCollection[action];
Nils Diewald5c5a7472015-04-02 22:13:38 +0000220 },
221
Akronda5bd3a2020-10-16 17:37:49 +0200222
Nils Diewald5c5a7472015-04-02 22:13:38 +0000223 /**
224 * Get the correct menu based on the context
225 */
226 contextMenu : function (ifContext) {
Akronda5bd3a2020-10-16 17:37:49 +0200227 const noC = ifContext ? undefined : this.menu("-");
Akron1a5a5872016-09-05 20:17:14 +0200228
Akron02360e42016-06-07 13:41:12 +0200229 // Get context (aka left text)
Akronda5bd3a2020-10-16 17:37:49 +0200230 let context = this._inputField.context();
Akronee9ef4a2016-06-03 12:50:08 +0200231
Akron65c74352016-09-02 17:23:39 +0200232 if (context === undefined || context.length === 0) {
Akronda5bd3a2020-10-16 17:37:49 +0200233 return noC;
Akron65c74352016-09-02 17:23:39 +0200234 };
Nils Diewald5c5a7472015-04-02 22:13:38 +0000235
Akron02360e42016-06-07 13:41:12 +0200236 // Get context (aka left text matching regex)
Nils Diewald5c5a7472015-04-02 22:13:38 +0000237 context = this._analyzer.test(context);
Akronee9ef4a2016-06-03 12:50:08 +0200238
Akron1a5a5872016-09-05 20:17:14 +0200239 if (context === undefined || context.length == 0) {
Akronda5bd3a2020-10-16 17:37:49 +0200240 return noC;
Akron1a5a5872016-09-05 20:17:14 +0200241 };
Nils Diewald5c5a7472015-04-02 22:13:38 +0000242
Akronda5bd3a2020-10-16 17:37:49 +0200243 return this.menu(context) || noC;
Nils Diewald5c5a7472015-04-02 22:13:38 +0000244 },
245
Akronda5bd3a2020-10-16 17:37:49 +0200246
Akron8eaeb2e2016-08-29 18:26:28 +0200247 /**
248 * Activate a certain menu.
249 * If a menu is passed, the menu will be activated.
250 * If null is passed, the active menu will be deactivated.
251 * If nothing is passed, returns the active menu.
252 */
Akron02360e42016-06-07 13:41:12 +0200253 active : function (obj) {
Akronda5bd3a2020-10-16 17:37:49 +0200254
Akron8eaeb2e2016-08-29 18:26:28 +0200255 // A menu or null was passed
Akronda5bd3a2020-10-16 17:37:49 +0200256 if (arguments.length === 1) {
257 const c = this._inputField.container();
Akrondadd1d12021-11-12 16:20:28 +0100258
Akron8eaeb2e2016-08-29 18:26:28 +0200259 // Make the menu active
Akronc14cbfc2018-08-31 13:15:55 +0200260 if (obj !== null) {
261 c.classList.add('active');
262 this._active = obj;
263 }
Akron8eaeb2e2016-08-29 18:26:28 +0200264
265 // Make the menu inactive
Akronc14cbfc2018-08-31 13:15:55 +0200266 else {
267 c.classList.remove('active');
268 this._active = null;
269 }
Akron00cd4d12016-05-31 21:01:11 +0200270 };
Akron8eaeb2e2016-08-29 18:26:28 +0200271
272 // Return
Akron00cd4d12016-05-31 21:01:11 +0200273 return this._active;
274 },
275
Nils Diewald5c5a7472015-04-02 22:13:38 +0000276
277 /**
Akron308db382016-05-30 22:34:07 +0200278 * Show the menu.
Akron65c74352016-09-02 17:23:39 +0200279 * Remove all old menus.
Akron02360e42016-06-07 13:41:12 +0200280 *
281 * @param {boolean} Boolean value to indicate if context
282 * is necessary (true) or if the main context should
283 * be shown if context fails.
284 *
Nils Diewald5c5a7472015-04-02 22:13:38 +0000285 */
286 show : function (ifContext) {
287
Akron1a5a5872016-09-05 20:17:14 +0200288 // Remove the active object
289 this._unshow();
Akronc14cbfc2018-08-31 13:15:55 +0200290
Nils Diewald5c5a7472015-04-02 22:13:38 +0000291 // Get the menu
Akronda5bd3a2020-10-16 17:37:49 +0200292 let menu;
Nils Diewald5c5a7472015-04-02 22:13:38 +0000293 if (menu = this.contextMenu(ifContext)) {
Akronda5bd3a2020-10-16 17:37:49 +0200294
Akronc14cbfc2018-08-31 13:15:55 +0200295 this.active(menu);
Akron02360e42016-06-07 13:41:12 +0200296
Akronda5bd3a2020-10-16 17:37:49 +0200297 let e = menu.element();
Akrondadd1d12021-11-12 16:20:28 +0100298 // Chrome may send a blur on the old menu here
Akronda5bd3a2020-10-16 17:37:49 +0200299 this._active.element().blur();
300 this._inputField.container().appendChild(e);
Akroncae907d2018-08-20 17:22:15 +0200301
Akronc14cbfc2018-08-31 13:15:55 +0200302 menu.show();
303 menu.focus();
304 // Focus on input field
305 // this.inputField.element.focus();
Akron65c74352016-09-02 17:23:39 +0200306 }
Akronda5bd3a2020-10-16 17:37:49 +0200307
Akron65c74352016-09-02 17:23:39 +0200308 else {
309 this._inputField.element().focus();
Nils Diewald19ccee92014-12-08 11:30:08 +0000310 };
Akron00cd4d12016-05-31 21:01:11 +0200311 },
Akron8eaeb2e2016-08-29 18:26:28 +0200312
Akronda5bd3a2020-10-16 17:37:49 +0200313
Akron02360e42016-06-07 13:41:12 +0200314 // This will get the context of the field
315 getContext : function () {},
316
Akron65c74352016-09-02 17:23:39 +0200317
318 /**
319 * Deactivate the current menu and focus on the input field.
320 */
321 unshow : function () {
Akron1a5a5872016-09-05 20:17:14 +0200322 this._unshow();
323 this._inputField.element().focus();
324 },
Akron65c74352016-09-02 17:23:39 +0200325
Akronc14cbfc2018-08-31 13:15:55 +0200326
327 // Catch touch events
328 _touch : function (e) {
Akronc14cbfc2018-08-31 13:15:55 +0200329 if (e.type === 'touchstart') {
Akronda5bd3a2020-10-16 17:37:49 +0200330 this._lastTouch = e.touches[0].clientY;
Akronc14cbfc2018-08-31 13:15:55 +0200331 }
Akronda5bd3a2020-10-16 17:37:49 +0200332
Akronc14cbfc2018-08-31 13:15:55 +0200333 else if (e.type === 'touchend') {
334 this._lastTouch = undefined;
335 }
Akronda5bd3a2020-10-16 17:37:49 +0200336
Akronc14cbfc2018-08-31 13:15:55 +0200337 else if (e.type == 'touchmove') {
Akronda5bd3a2020-10-16 17:37:49 +0200338 if ((this._lastTouch + 10) < e.touches[0].clientY) {
Akronc14cbfc2018-08-31 13:15:55 +0200339 this.show();
340 this._lastTouch = undefined;
341 };
342 e.halt();
343 }
344 },
345
Akronda5bd3a2020-10-16 17:37:49 +0200346
347 // Unshow the hint menu
Akron1a5a5872016-09-05 20:17:14 +0200348 _unshow : function () {
Akron65c74352016-09-02 17:23:39 +0200349 if (this.active() !== null) {
Akronda5bd3a2020-10-16 17:37:49 +0200350
Akron65c74352016-09-02 17:23:39 +0200351 // This does not work for alert currently!
Akron1a5a5872016-09-05 20:17:14 +0200352 if (!this._alert.active) {
Akroncae907d2018-08-20 17:22:15 +0200353
Akronda5bd3a2020-10-16 17:37:49 +0200354 // TODO: This does not work for webkit?
355 this._inputField
356 .container()
357 .removeChild(this._active.element());
Akronc14cbfc2018-08-31 13:15:55 +0200358 }
Akronda5bd3a2020-10-16 17:37:49 +0200359
Akron1a5a5872016-09-05 20:17:14 +0200360 else {
361 this._unshowAlert();
362 };
363
Akron65c74352016-09-02 17:23:39 +0200364 this.active(null);
365 };
Akronda5bd3a2020-10-16 17:37:49 +0200366 },
367
368 // Unshow alert
369 _unshowAlert : function () {
370 this._alert.hide();
371 this.active(null);
Nils Diewald19ccee92014-12-08 11:30:08 +0000372 }
373 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000374});