blob: 865c3ec5d9474925e6c61c7de6e9412272ac9fcf [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) {
Leo Repp050a7342021-10-25 11:05:32 +0200168 //There are only four new lines, marked with NEW
Leo Reppd162b2e2021-06-30 13:51:07 +0200169 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
Leo Repp050a7342021-10-25 11:05:32 +0200180 t._prefix.active(true);
181 t.container().makeActive(); //NEW Incase the own
182 // list becomes empty we need to make container active for line 129 to work
Leo Reppd162b2e2021-06-30 13:51:07 +0200183
184 // finally show the element
Leo Repp050a7342021-10-25 11:05:32 +0200185 t._el.classList.add('visible');
186 t.container()._el.classList.add('visible'); //NEW
Leo Reppd162b2e2021-06-30 13:51:07 +0200187
188 return true;
189 };
190
191 let offset = 0;
192
193 // Set a chosen value to active and move the viewport
194 if (arguments.length === 1) {
195
196 // Normalize active value
197 if (active < 0) {
198 active = 0;
199 }
200 else if (active >= t.liveLength()) {
201 active = t.liveLength() - 1;
202 };
203
204 // Item is outside the first viewport
205 if (active >= t._limit) {
206 offset = active;
207 const newOffset = t.liveLength() - t._limit;
208 if (offset > newOffset) {
209 offset = newOffset;
210 };
211 };
212
213 t.position = active;
214 }
215
216 // Choose the first item
217 else if (t._firstActive) {
218 t.position = 0;
219 }
220
221 // Choose no item
222 else {
223 t.position = -1;
224 };
225
226 t.offset = offset;
227 t._showItems(offset); // Show new item list
228
229 // Make chosen value active
230 if (t.position !== -1) {
231 t.liveItem(t.position).active(true);
232 };
233
234 // The prefix is not active
235 t._prefix.active(false);
236
237 // finally show the element
238 t._el.classList.add('visible');
239 t.container()._el.classList.add('visible'); //NEW
240
241 // Add classes for rolling menus
242 t._boundary(true);
243
244 return true;
245 },
246
247
248 /**
249 * Hide the menu and call the onHide callback.
250 */
251 hide : function () { //only one new line
252 if (!this.dontHide) {
253 this.removeItems();
254 this._prefix.clear();
255 this.onHide();
256 this._el.classList.remove('visible');
257 this.container()._el.classList.remove('visible'); //NEW
258 }
259 // this._el.blur();
260 },
261
262
263
264 /**
265 * Make the next item in the filtered menu active
266 */
267 next : function () {
268 const t = this;
269 var notInContainerAnyMore;
270 const c = t.container();
271 const cLLength = c.liveLength();
272 // No list
273 if (t.liveLength()===0){
274 if (cLLength === 0) return;
275 notInContainerAnyMore = c.next();
276 if (notInContainerAnyMore) {
277 c.next();
278 }
279 return;
280 };
281 if (!c.active() && t.position!==-1) {t.liveItem(t.position).active(false);} //this should be enough to ensure a valid t.position
282 if (!c.active()){
283 t.position++;
284 };
285 let newItem = t.liveItem(t.position); //progress
286 if (newItem === undefined) { //too far
287 t.position = -1;
288 if (cLLength !== 0){ //actually makes sense to next
289 notInContainerAnyMore = t.container().next(); //activate container
290 if (notInContainerAnyMore) { //oh, next one (should not happen, because cLLength is now liveLength)
291 t.position = 0;
292 t._showItems(0);
293 newItem=t.liveItem(0);
294 };
295 } else {
296 t.position = 0;
297 t._showItems(0);
298 newItem=t.liveItem(0);
299 };
300 }// The next element is after the viewport - roll down
301 else if (t.position >= (t.limit() + t.offset)) {
302 t.screen(t.position - t.limit() + 1);
303 }
304 // The next element is before the viewport - roll up
305 else if (t.position <= t.offset) {
306 t.screen(t.position);
307 }
308 if (newItem !== undefined) {
309 newItem.active(true);
310 };
311 },
312
313
314 /**
315 * Make the previous item in the menu active
316 */
317 prev : function () {
318 const t = this;
319 var notInContainerAnyMore;
320 const c = t.container();
321 const cLLength = c.liveLength();
322
323 // No list
324 if (t.liveLength() === 0) {
325 if (cLLength === 0) return;
326 notInContainerAnyMore = c.prev();
327 if (notInContainerAnyMore) {
328 c.prev();
329 }
330 return;
331 }
332 if (!c.active() && t.position!==-1) {t.liveItem(t.position).active(false);}//this should be enough to ensure a valid t.position
333 if (!c.active()){
334 t.position--;
335 };
336 let newItem = t.liveItem(t.position); //progress
337 if (newItem === undefined) { //too far
338 t.position = -1;
339 let offset = t.liveLength() - t.limit();
340 // Normalize offset
341 offset = offset < 0 ? 0 : offset;
342 if (cLLength !== 0){ //actually makes sense to next
343 notInContainerAnyMore = t.container().prev(); //activate container
344 if (notInContainerAnyMore) { //oh, next one (should not happen, because cLLength is now liveLength)
345 t.position = t.liveLength() - 1;
346 newItem = t.liveItem(t.position);
347 t._showItems(offset);
348 } else {
349 t.offset = offset;
350 };
351 } else {
352 t.position = t.liveLength() - 1;
353 newItem = t.liveItem(t.position);
354 t._showItems(offset);
355 }
356 }
357 // The previous element is before the view - roll up
358 else if (t.position < t.offset) {
359 t.screen(t.position);
360 }
361
362 // The previous element is after the view - roll down
363 else if (t.position >= (t.limit() + t.offset)) {
364 t.screen(t.position - t.limit() + 2);
365 };
366 if (newItem !== undefined) {
367 newItem.active(true);
368 };
369 },
370
371 /**
372 * Get the container object
373 */
374 container : function () {
375 return this._container;
376 }
377
378
379 };
380});