blob: 09580457fcfc55e37b1970a390c7bc14b39103c7 [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 Repp8e21cbe2021-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 Repp8e21cbe2021-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 Repp8e21cbe2021-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 Repp8e21cbe2021-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 Repp8e21cbe2021-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
133 t.prefix("");
134 t.hint().inputField().insert("").update();
135 t.hide();
136 t.hint().unshow();
137 //for clicking this is done in container.js with an eventListener for click.
Leo Reppd162b2e2021-06-30 13:51:07 +0200138 } else { // Click on item
139 t.liveItem(t.position).onclick(e);
Leo Repp8e21cbe2021-08-18 16:37:52 +0200140 //Above is already done: see file dev/js/src/hint/item.js
141
Leo Reppd162b2e2021-06-30 13:51:07 +0200142 };
143 e.halt();
144 break;
145
146 case 8: // 'Backspace'
147 t.container().chop();
148 t.show();
149 e.halt();
150 break;
151 };
152 },
153
154
155 // Add characters to prefix and other interested items
156 _keypress : function (e) {
157 if (e.charCode !== 0) {
158 e.halt();
159
160 // Add prefix and other interested items
161 this.container().add(
162 String.fromCharCode(_codeFromEvent(e))
163 );
164
165 this.show();
166 };
167 },
168
169
170 /**
171 * Filter the list and make it visible.
172 * This is always called once the prefix changes.
173 *
174 * @param {string} Prefix for filtering the list
175 */
176 show : function (active) {
Leo Repp8e21cbe2021-08-18 16:37:52 +0200177 //There are only five new lines, marked with NEW
Leo Reppd162b2e2021-06-30 13:51:07 +0200178 const t = this;
179
180 // show menu based on initial offset
181 t._unmark(); // Unmark everything that was marked before
182 t.removeItems();
183 t.container().exit(); //NEW
184
185 // Initialize the list
186 if (!t._initList()) {
187
188 // The prefix is not active
Leo Repp8e21cbe2021-08-18 16:37:52 +0200189 //t._prefix.active(true); //NEW: not used
Leo Repp050a7342021-10-25 11:05:32 +0200190 t.container().makeActive(); //NEW Incase the own
191 // list becomes empty we need to make container active for line 129 to work
Leo Reppd162b2e2021-06-30 13:51:07 +0200192
193 // finally show the element
Leo Repp050a7342021-10-25 11:05:32 +0200194 t._el.classList.add('visible');
195 t.container()._el.classList.add('visible'); //NEW
Leo Reppd162b2e2021-06-30 13:51:07 +0200196
197 return true;
198 };
199
200 let offset = 0;
201
202 // Set a chosen value to active and move the viewport
203 if (arguments.length === 1) {
204
205 // Normalize active value
206 if (active < 0) {
207 active = 0;
208 }
209 else if (active >= t.liveLength()) {
210 active = t.liveLength() - 1;
211 };
212
213 // Item is outside the first viewport
214 if (active >= t._limit) {
215 offset = active;
216 const newOffset = t.liveLength() - t._limit;
217 if (offset > newOffset) {
218 offset = newOffset;
219 };
220 };
221
222 t.position = active;
223 }
224
225 // Choose the first item
226 else if (t._firstActive) {
227 t.position = 0;
228 }
229
230 // Choose no item
231 else {
232 t.position = -1;
233 };
234
235 t.offset = offset;
236 t._showItems(offset); // Show new item list
237
238 // Make chosen value active
239 if (t.position !== -1) {
240 t.liveItem(t.position).active(true);
241 };
242
243 // The prefix is not active
244 t._prefix.active(false);
245
246 // finally show the element
247 t._el.classList.add('visible');
248 t.container()._el.classList.add('visible'); //NEW
249
250 // Add classes for rolling menus
251 t._boundary(true);
252
253 return true;
254 },
255
256
257 /**
258 * Hide the menu and call the onHide callback.
259 */
260 hide : function () { //only one new line
261 if (!this.dontHide) {
262 this.removeItems();
263 this._prefix.clear();
264 this.onHide();
265 this._el.classList.remove('visible');
Leo Repp8e21cbe2021-08-18 16:37:52 +0200266 this.container()._el.classList.remove('visible'); //NEW //apparently not necessary
Leo Reppd162b2e2021-06-30 13:51:07 +0200267 }
268 // this._el.blur();
269 },
270
271
272
273 /**
274 * Make the next item in the filtered menu active
275 */
276 next : function () {
277 const t = this;
278 var notInContainerAnyMore;
279 const c = t.container();
280 const cLLength = c.liveLength();
281 // No list
282 if (t.liveLength()===0){
283 if (cLLength === 0) return;
284 notInContainerAnyMore = c.next();
285 if (notInContainerAnyMore) {
286 c.next();
287 }
288 return;
289 };
290 if (!c.active() && t.position!==-1) {t.liveItem(t.position).active(false);} //this should be enough to ensure a valid t.position
291 if (!c.active()){
292 t.position++;
293 };
294 let newItem = t.liveItem(t.position); //progress
295 if (newItem === undefined) { //too far
296 t.position = -1;
297 if (cLLength !== 0){ //actually makes sense to next
298 notInContainerAnyMore = t.container().next(); //activate container
299 if (notInContainerAnyMore) { //oh, next one (should not happen, because cLLength is now liveLength)
300 t.position = 0;
301 t._showItems(0);
302 newItem=t.liveItem(0);
303 };
304 } else {
305 t.position = 0;
306 t._showItems(0);
307 newItem=t.liveItem(0);
308 };
309 }// The next element is after the viewport - roll down
310 else if (t.position >= (t.limit() + t.offset)) {
311 t.screen(t.position - t.limit() + 1);
312 }
313 // The next element is before the viewport - roll up
314 else if (t.position <= t.offset) {
315 t.screen(t.position);
316 }
317 if (newItem !== undefined) {
318 newItem.active(true);
319 };
320 },
321
322
323 /**
324 * Make the previous item in the menu active
325 */
326 prev : function () {
327 const t = this;
328 var notInContainerAnyMore;
329 const c = t.container();
330 const cLLength = c.liveLength();
331
332 // No list
333 if (t.liveLength() === 0) {
334 if (cLLength === 0) return;
335 notInContainerAnyMore = c.prev();
336 if (notInContainerAnyMore) {
337 c.prev();
338 }
339 return;
340 }
341 if (!c.active() && t.position!==-1) {t.liveItem(t.position).active(false);}//this should be enough to ensure a valid t.position
342 if (!c.active()){
343 t.position--;
344 };
345 let newItem = t.liveItem(t.position); //progress
346 if (newItem === undefined) { //too far
347 t.position = -1;
348 let offset = t.liveLength() - t.limit();
349 // Normalize offset
350 offset = offset < 0 ? 0 : offset;
351 if (cLLength !== 0){ //actually makes sense to next
352 notInContainerAnyMore = t.container().prev(); //activate container
353 if (notInContainerAnyMore) { //oh, next one (should not happen, because cLLength is now liveLength)
354 t.position = t.liveLength() - 1;
355 newItem = t.liveItem(t.position);
356 t._showItems(offset);
357 } else {
358 t.offset = offset;
359 };
360 } else {
361 t.position = t.liveLength() - 1;
362 newItem = t.liveItem(t.position);
363 t._showItems(offset);
364 }
365 }
366 // The previous element is before the view - roll up
367 else if (t.position < t.offset) {
368 t.screen(t.position);
369 }
370
371 // The previous element is after the view - roll down
372 else if (t.position >= (t.limit() + t.offset)) {
373 t.screen(t.position - t.limit() + 2);
374 };
375 if (newItem !== undefined) {
376 newItem.active(true);
377 };
378 },
Leo Repp8e21cbe2021-08-18 16:37:52 +0200379
380 /**
381 * Upgrade this object to another object,
382 * while private data stays intact.
383 *
384 * @param {Object} An object with properties.
385 */
386 upgradeTo : function (props) {
387 for (var prop in props) {
388 this[prop] = props[prop];
389 };
390 return this;
391 },
Leo Reppd162b2e2021-06-30 13:51:07 +0200392
393 /**
394 * Get the container object
395 */
396 container : function () {
397 return this._container;
398 }
399
400
401 };
402});