blob: a272e10f83190cb0bee24659d1d9709dc038dfe7 [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,
15 defaultContainerClass) {
16
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 }
44 obj.container().addMenu(obj);
45
46 // add entry to HTML element
47 obj._el.appendChild(obj.container().element());
48 obj._el.removeChild(obj._prefix.element());
49 //Keep prefix as 'pref' style. The additional distance is fine.
50 obj.container().addPrefix(obj._prefix);
51 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);
131 } else { // Click on item
132 t.liveItem(t.position).onclick(e);
133 };
134 e.halt();
135 break;
136
137 case 8: // 'Backspace'
138 t.container().chop();
139 t.show();
140 e.halt();
141 break;
142 };
143 },
144
145
146 // Add characters to prefix and other interested items
147 _keypress : function (e) {
148 if (e.charCode !== 0) {
149 e.halt();
150
151 // Add prefix and other interested items
152 this.container().add(
153 String.fromCharCode(_codeFromEvent(e))
154 );
155
156 this.show();
157 };
158 },
159
160
161 /**
162 * Filter the list and make it visible.
163 * This is always called once the prefix changes.
164 *
165 * @param {string} Prefix for filtering the list
166 */
167 show : function (active) {
168 //there are only two new lines, marked with NEW
169 const t = this;
170
171 // show menu based on initial offset
172 t._unmark(); // Unmark everything that was marked before
173 t.removeItems();
174 t.container().exit(); //NEW
175
176 // Initialize the list
177 if (!t._initList()) {
178
179 // The prefix is not active
180 t._prefix.active(true);
181
182 // finally show the element
183 t._el.classList.add('visible'); // TODO do I need this for container?
184 t.container()._el.classList.add('visible');
185
186 return true;
187 };
188
189 let offset = 0;
190
191 // Set a chosen value to active and move the viewport
192 if (arguments.length === 1) {
193
194 // Normalize active value
195 if (active < 0) {
196 active = 0;
197 }
198 else if (active >= t.liveLength()) {
199 active = t.liveLength() - 1;
200 };
201
202 // Item is outside the first viewport
203 if (active >= t._limit) {
204 offset = active;
205 const newOffset = t.liveLength() - t._limit;
206 if (offset > newOffset) {
207 offset = newOffset;
208 };
209 };
210
211 t.position = active;
212 }
213
214 // Choose the first item
215 else if (t._firstActive) {
216 t.position = 0;
217 }
218
219 // Choose no item
220 else {
221 t.position = -1;
222 };
223
224 t.offset = offset;
225 t._showItems(offset); // Show new item list
226
227 // Make chosen value active
228 if (t.position !== -1) {
229 t.liveItem(t.position).active(true);
230 };
231
232 // The prefix is not active
233 t._prefix.active(false);
234
235 // finally show the element
236 t._el.classList.add('visible');
237 t.container()._el.classList.add('visible'); //NEW
238
239 // Add classes for rolling menus
240 t._boundary(true);
241
242 return true;
243 },
244
245
246 /**
247 * Hide the menu and call the onHide callback.
248 */
249 hide : function () { //only one new line
250 if (!this.dontHide) {
251 this.removeItems();
252 this._prefix.clear();
253 this.onHide();
254 this._el.classList.remove('visible');
255 this.container()._el.classList.remove('visible'); //NEW
256 }
257 // this._el.blur();
258 },
259
260
261
262 /**
263 * Make the next item in the filtered menu active
264 */
265 next : function () {
266 const t = this;
267 var notInContainerAnyMore;
268 const c = t.container();
269 const cLLength = c.liveLength();
270 // No list
271 if (t.liveLength()===0){
272 if (cLLength === 0) return;
273 notInContainerAnyMore = c.next();
274 if (notInContainerAnyMore) {
275 c.next();
276 }
277 return;
278 };
279 if (!c.active() && t.position!==-1) {t.liveItem(t.position).active(false);} //this should be enough to ensure a valid t.position
280 if (!c.active()){
281 t.position++;
282 };
283 let newItem = t.liveItem(t.position); //progress
284 if (newItem === undefined) { //too far
285 t.position = -1;
286 if (cLLength !== 0){ //actually makes sense to next
287 notInContainerAnyMore = t.container().next(); //activate container
288 if (notInContainerAnyMore) { //oh, next one (should not happen, because cLLength is now liveLength)
289 t.position = 0;
290 t._showItems(0);
291 newItem=t.liveItem(0);
292 };
293 } else {
294 t.position = 0;
295 t._showItems(0);
296 newItem=t.liveItem(0);
297 };
298 }// The next element is after the viewport - roll down
299 else if (t.position >= (t.limit() + t.offset)) {
300 t.screen(t.position - t.limit() + 1);
301 }
302 // The next element is before the viewport - roll up
303 else if (t.position <= t.offset) {
304 t.screen(t.position);
305 }
306 if (newItem !== undefined) {
307 newItem.active(true);
308 };
309 },
310
311
312 /**
313 * Make the previous item in the menu active
314 */
315 prev : function () {
316 const t = this;
317 var notInContainerAnyMore;
318 const c = t.container();
319 const cLLength = c.liveLength();
320
321 // No list
322 if (t.liveLength() === 0) {
323 if (cLLength === 0) return;
324 notInContainerAnyMore = c.prev();
325 if (notInContainerAnyMore) {
326 c.prev();
327 }
328 return;
329 }
330 if (!c.active() && t.position!==-1) {t.liveItem(t.position).active(false);}//this should be enough to ensure a valid t.position
331 if (!c.active()){
332 t.position--;
333 };
334 let newItem = t.liveItem(t.position); //progress
335 if (newItem === undefined) { //too far
336 t.position = -1;
337 let offset = t.liveLength() - t.limit();
338 // Normalize offset
339 offset = offset < 0 ? 0 : offset;
340 if (cLLength !== 0){ //actually makes sense to next
341 notInContainerAnyMore = t.container().prev(); //activate container
342 if (notInContainerAnyMore) { //oh, next one (should not happen, because cLLength is now liveLength)
343 t.position = t.liveLength() - 1;
344 newItem = t.liveItem(t.position);
345 t._showItems(offset);
346 } else {
347 t.offset = offset;
348 };
349 } else {
350 t.position = t.liveLength() - 1;
351 newItem = t.liveItem(t.position);
352 t._showItems(offset);
353 }
354 }
355 // The previous element is before the view - roll up
356 else if (t.position < t.offset) {
357 t.screen(t.position);
358 }
359
360 // The previous element is after the view - roll down
361 else if (t.position >= (t.limit() + t.offset)) {
362 t.screen(t.position - t.limit() + 2);
363 };
364 if (newItem !== undefined) {
365 newItem.active(true);
366 };
367 },
368
369 /**
370 * Get the container object
371 */
372 container : function () {
373 return this._container;
374 }
375
376
377 };
378});