blob: 5d3f5f3ba080d5ee9000a23d488d0e75124adb5a [file] [log] [blame]
Leo Reppd162b2e2021-06-30 13:51:07 +02001/**
2 * Menu with a container for always visible non scrollable items (can also be made invisible)
3 * Automatically moves the prefix into the container. See containeritem.js for an API of functions
4 * a container will call on containeritem.
5 *
6 * @author Leo Repp, with reused code by Nils Diewald
7 */
8
9"use strict";
10define([
11 'menu',
12 'container/container',
13 'util'
14], function (defaultMenuClass,
Leo Repp57997402021-08-18 16:37:52 +020015 defaultContainerClass) {
Leo Reppd162b2e2021-06-30 13:51:07 +020016
17 return {
18 /**
19 * Create new Container Menu based on the action prefix
20 * and a list of menu items.
21 *
22 * Accepts an associative array containg the elements
23 * itemClass, prefixClass, lengthFieldClass, containerClass, containerItemClass
24 *
25 * @this {Menu}
26 * @constructor
27 * @param {string} params Context prefix
28 * @param {Array.<Array.<string>>} list List of menu items
29 * @param {Array.<Array.<containerItem>>} containerList List of container items
30 */
31 create : function (list, params, containerList) {
32 const obj = defaultMenuClass.create(list, params)
33 .upgradeTo(this)
34 ._init(list, params);
35
36 obj._el.classList.add('containermenu');
37
38 //add container object and allow for own containerClasses
39 if (params!==undefined && params["containerClass"] !== undefined) {
40 obj._container = params["containerClass"].create(containerList, params);
41 } else {
42 obj._container = defaultContainerClass.create(containerList, params);
43 }
Leo Repp57997402021-08-18 16:37:52 +020044 obj.container().addMenu(obj); //this is your menu, container!
Leo Reppd162b2e2021-06-30 13:51:07 +020045
46 // add entry to HTML element
47 obj._el.appendChild(obj.container().element());
Leo Repp57997402021-08-18 16:37:52 +020048 obj._el.removeChild(obj._prefix.element()); //different HTML element relationship required
Leo Reppd162b2e2021-06-30 13:51:07 +020049 //Keep prefix as 'pref' style. The additional distance is fine.
Leo Repp57997402021-08-18 16:37:52 +020050 obj.container().addPrefix(obj._prefix); //creates containeritem as base for prefix that then is upgraded to prefix. Also ajust _menu chains.
Leo Reppd162b2e2021-06-30 13:51:07 +020051 return obj;
52 },
53
54 /**
55 * Destroy this menu
56 * (in case you don't trust the
57 * mark and sweep GC)!
58 */
59 destroy : function () {
60 // Upon change also update alwaysmenu.js please
61 const t = this;
62
63 // Remove circular reference to "this" in menu
64 if (t._el != undefined)
65 delete t._el["menu"];
66
67 // Remove circular reference to "this" in items
68 t._items.forEach(function(i) {
69 delete i["_menu"];
70 });
71
72 // Remove circular reference to "this" in prefix
73 delete t._prefix['_menu'];
74 delete t._lengthField['_menu'];
75 delete t._slider['_menu'];
76 t.container().destroy();
77 delete t.container()['_menu'];
78 },
79
80 // Arrow key and container treatment
81 _keydown : function (e) {
82 const t = this;
83
84 switch (_codeFromEvent(e)) {
85
86 case 27: // 'Esc'
87 e.halt();
88 t.hide();
89 break;
90
91 case 38: // 'Up'
92 e.halt();
93 t.prev();
94 break;
95
96 case 33: // 'Page up'
97 e.halt();
98 t.pageUp();
99 break;
100
101 case 40: // 'Down'
102 e.halt();
103 t.next();
104 break;
105
106 case 34: // 'Page down'
107 e.halt();
108 t.pageDown();
109 break;
110
111 case 39: // 'Right'
112 if (t.container().active()){
113 t.container().further();
114 e.halt();
115 break;
116 }
117
118 const item = t.liveItem(t.position);
119
120 if (item["further"] !== undefined) {
121 item["further"].bind(item).apply();
122 };
123
124 e.halt();
125 break;
126
127 case 13: // 'Enter'
128 // Click on prefix
129 if (t.container().active()){
130 t.container().enter(e);
Leo Repp57997402021-08-18 16:37:52 +0200131 //NEW: reset some things. These are reset for hint menu style items
132 // so I believe we need to do the same when pressing on items in the container
Leo Repp46903bf2021-12-18 16:05:53 +0100133 //t.reset("");
134 //t.hide();
135 //t.hint().unshow(); Moved this line into hint/menu.js because it is hint containermenu specific!
Leo Repp57997402021-08-18 16:37:52 +0200136 //for clicking this is done in container.js with an eventListener for click.
Leo Reppd162b2e2021-06-30 13:51:07 +0200137 } else { // Click on item
138 t.liveItem(t.position).onclick(e);
Leo Repp57997402021-08-18 16:37:52 +0200139 //Above is already done: see file dev/js/src/hint/item.js
140
Leo Reppd162b2e2021-06-30 13:51:07 +0200141 };
142 e.halt();
143 break;
144
145 case 8: // 'Backspace'
146 t.container().chop();
147 t.show();
148 e.halt();
149 break;
150 };
151 },
152
153
154 // Add characters to prefix and other interested items
155 _keypress : function (e) {
156 if (e.charCode !== 0) {
157 e.halt();
158
159 // Add prefix and other interested items
160 this.container().add(
161 String.fromCharCode(_codeFromEvent(e))
162 );
163
164 this.show();
165 };
166 },
167
168
169 /**
170 * Filter the list and make it visible.
171 * This is always called once the prefix changes.
172 *
173 * @param {string} Prefix for filtering the list
174 */
175 show : function (active) {
Leo Reppd162b2e2021-06-30 13:51:07 +0200176 const t = this;
177
178 // show menu based on initial offset
179 t._unmark(); // Unmark everything that was marked before
180 t.removeItems();
Leo Repp57997402021-08-18 16:37:52 +0200181 t.container().exit();
Leo Reppd162b2e2021-06-30 13:51:07 +0200182
183 // Initialize the list
184 if (!t._initList()) {
185
186 // The prefix is not active
Leo Repp57997402021-08-18 16:37:52 +0200187 //t._prefix.active(true);
188 t.container().makeActive(); //Incase the own
189 // list becomes empty we need to make container active for handling ENTER keypress to work
Leo Reppd162b2e2021-06-30 13:51:07 +0200190
191 // finally show the element
Leo Repp050a7342021-10-25 11:05:32 +0200192 t._el.classList.add('visible');
Leo Repp57997402021-08-18 16:37:52 +0200193 t.container()._el.classList.add('visible');
Leo Reppd162b2e2021-06-30 13:51:07 +0200194
195 return true;
196 };
197
198 let offset = 0;
199
200 // Set a chosen value to active and move the viewport
201 if (arguments.length === 1) {
202
203 // Normalize active value
204 if (active < 0) {
205 active = 0;
206 }
207 else if (active >= t.liveLength()) {
208 active = t.liveLength() - 1;
209 };
210
211 // Item is outside the first viewport
212 if (active >= t._limit) {
213 offset = active;
214 const newOffset = t.liveLength() - t._limit;
215 if (offset > newOffset) {
216 offset = newOffset;
217 };
218 };
219
220 t.position = active;
221 }
222
223 // Choose the first item
224 else if (t._firstActive) {
225 t.position = 0;
226 }
227
228 // Choose no item
229 else {
230 t.position = -1;
231 };
232
233 t.offset = offset;
234 t._showItems(offset); // Show new item list
235
236 // Make chosen value active
237 if (t.position !== -1) {
238 t.liveItem(t.position).active(true);
239 };
240
241 // The prefix is not active
242 t._prefix.active(false);
243
244 // finally show the element
245 t._el.classList.add('visible');
Leo Repp57997402021-08-18 16:37:52 +0200246 t.container()._el.classList.add('visible');
Leo Reppd162b2e2021-06-30 13:51:07 +0200247
248 // Add classes for rolling menus
249 t._boundary(true);
250
251 return true;
252 },
253
254
255 /**
256 * Hide the menu and call the onHide callback.
257 */
258 hide : function () { //only one new line
259 if (!this.dontHide) {
260 this.removeItems();
261 this._prefix.clear();
262 this.onHide();
263 this._el.classList.remove('visible');
Leo Repp57997402021-08-18 16:37:52 +0200264 this.container()._el.classList.remove('visible'); //NEW //apparently not necessary
Leo Reppd162b2e2021-06-30 13:51:07 +0200265 }
266 // this._el.blur();
267 },
268
269
270
271 /**
272 * Make the next item in the filtered menu active
273 */
274 next : function () {
275 const t = this;
276 var notInContainerAnyMore;
277 const c = t.container();
278 const cLLength = c.liveLength();
279 // No list
280 if (t.liveLength()===0){
281 if (cLLength === 0) return;
282 notInContainerAnyMore = c.next();
283 if (notInContainerAnyMore) {
284 c.next();
285 }
286 return;
287 };
288 if (!c.active() && t.position!==-1) {t.liveItem(t.position).active(false);} //this should be enough to ensure a valid t.position
289 if (!c.active()){
290 t.position++;
291 };
292 let newItem = t.liveItem(t.position); //progress
293 if (newItem === undefined) { //too far
294 t.position = -1;
295 if (cLLength !== 0){ //actually makes sense to next
296 notInContainerAnyMore = t.container().next(); //activate container
297 if (notInContainerAnyMore) { //oh, next one (should not happen, because cLLength is now liveLength)
298 t.position = 0;
299 t._showItems(0);
300 newItem=t.liveItem(0);
301 };
302 } else {
303 t.position = 0;
304 t._showItems(0);
305 newItem=t.liveItem(0);
306 };
307 }// The next element is after the viewport - roll down
308 else if (t.position >= (t.limit() + t.offset)) {
309 t.screen(t.position - t.limit() + 1);
310 }
311 // The next element is before the viewport - roll up
312 else if (t.position <= t.offset) {
313 t.screen(t.position);
314 }
315 if (newItem !== undefined) {
316 newItem.active(true);
317 };
318 },
319
320
321 /**
322 * Make the previous item in the menu active
323 */
324 prev : function () {
325 const t = this;
326 var notInContainerAnyMore;
327 const c = t.container();
328 const cLLength = c.liveLength();
329
330 // No list
331 if (t.liveLength() === 0) {
332 if (cLLength === 0) return;
333 notInContainerAnyMore = c.prev();
334 if (notInContainerAnyMore) {
335 c.prev();
336 }
337 return;
338 }
339 if (!c.active() && t.position!==-1) {t.liveItem(t.position).active(false);}//this should be enough to ensure a valid t.position
340 if (!c.active()){
341 t.position--;
342 };
343 let newItem = t.liveItem(t.position); //progress
344 if (newItem === undefined) { //too far
345 t.position = -1;
346 let offset = t.liveLength() - t.limit();
347 // Normalize offset
348 offset = offset < 0 ? 0 : offset;
349 if (cLLength !== 0){ //actually makes sense to next
350 notInContainerAnyMore = t.container().prev(); //activate container
351 if (notInContainerAnyMore) { //oh, next one (should not happen, because cLLength is now liveLength)
352 t.position = t.liveLength() - 1;
353 newItem = t.liveItem(t.position);
354 t._showItems(offset);
355 } else {
356 t.offset = offset;
357 };
358 } else {
359 t.position = t.liveLength() - 1;
360 newItem = t.liveItem(t.position);
361 t._showItems(offset);
362 }
363 }
364 // The previous element is before the view - roll up
365 else if (t.position < t.offset) {
366 t.screen(t.position);
367 }
368
369 // The previous element is after the view - roll down
370 else if (t.position >= (t.limit() + t.offset)) {
371 t.screen(t.position - t.limit() + 2);
372 };
373 if (newItem !== undefined) {
374 newItem.active(true);
375 };
376 },
377
378 /**
379 * Get the container object
380 */
381 container : function () {
382 return this._container;
383 }
384
385
386 };
387});