Leo Repp | 56904d2 | 2021-04-26 15:53:22 +0200 | [diff] [blame] | 1 | /** |
| 2 | * |
| 3 | * A Version of the menu class, that |
| 4 | * has an always displayed entry that can be |
| 5 | * clicked and contains text |
| 6 | * |
| 7 | * This entry button may or may not be displayed on top of objects |
| 8 | * lying under (>y) this menu. See alwaysentry update: negative absolute |
| 9 | * y coordinate. |
| 10 | * |
| 11 | * @author Leo Repp |
| 12 | */ |
| 13 | |
| 14 | "use strict"; |
| 15 | define([ |
| 16 | 'menu', |
| 17 | 'menu/item', |
| 18 | 'menu/prefix', |
| 19 | 'menu/lengthField', |
| 20 | 'alwaysentry', |
| 21 | 'util' |
| 22 | ], function ( |
| 23 | menuClass, |
| 24 | defaultItemClass, |
| 25 | defaultPrefixClass, |
| 26 | defaultLengthFieldClass, |
| 27 | defaultAlwaysEntryClass) { |
| 28 | |
| 29 | return { |
| 30 | |
| 31 | /** |
| 32 | * Create new menu with an always visible entry. |
| 33 | * @this {AlwaysMenu} |
| 34 | * @constructor |
| 35 | * @param {Object["like this"]} params Object with attributes prefixCLass, itemClass, lengthFieldClass, alwaysEntryClass |
| 36 | * @param {Array.<Array.<string>>} list list of menu items |
| 37 | */ |
| 38 | create : function (list, params) { |
| 39 | const obj = menuClass.create(list,params) |
| 40 | .upgradeTo(this) |
| 41 | ._init(list, params); |
| 42 | |
| 43 | obj._el.classList.add('alwaysmenu'); |
| 44 | |
| 45 | //add entry object and allow for own entryClasses |
| 46 | if (params!==undefined && params["alwaysEntryClass"] !== undefined) { |
| 47 | obj._entry = params["alwaysEntryClass"].create(); |
| 48 | } else { |
| 49 | obj._entry=defaultAlwaysEntryClass.create(); |
| 50 | } |
| 51 | obj._entry._menu=obj; |
| 52 | obj._notItemElements=4; |
| 53 | // add entry to HTML element |
| 54 | obj._el.appendChild(obj._entry.element()); |
| 55 | |
| 56 | return obj; |
| 57 | }, |
| 58 | |
| 59 | |
| 60 | /** |
| 61 | * Destroy this menu |
| 62 | * (in case you don't trust the |
| 63 | * mark and sweep GC)! |
| 64 | */ |
| 65 | destroy : function () { |
| 66 | //based on menu.js |
| 67 | const t = this; |
| 68 | |
| 69 | // Remove circular reference to "this" in menu |
| 70 | if (t._el != undefined) |
| 71 | delete t._el["menu"]; |
| 72 | |
| 73 | // Remove circular reference to "this" in items |
| 74 | t._items.forEach(function(i) { |
| 75 | delete i["_menu"]; |
| 76 | }); |
| 77 | |
| 78 | // Remove circular reference to "this" in prefix |
| 79 | delete t._prefix['_menu']; |
| 80 | delete t._lengthField['_menu']; |
| 81 | delete t._slider['_menu']; |
| 82 | delete t._entry['_menu']; |
| 83 | }, |
| 84 | |
| 85 | |
| 86 | // Arrow key and prefix treatment |
| 87 | _keydown : function (e) { |
| 88 | //based on menu.js |
| 89 | const t = this; |
| 90 | |
| 91 | switch (_codeFromEvent(e)) { |
| 92 | |
| 93 | case 27: // 'Esc' |
| 94 | e.halt(); |
| 95 | t.hide(); |
| 96 | break; |
| 97 | |
| 98 | case 38: // 'Up' |
| 99 | e.halt(); |
| 100 | t.prev(); |
| 101 | break; |
| 102 | |
| 103 | case 33: // 'Page up' |
| 104 | e.halt(); |
| 105 | t.pageUp(); |
| 106 | break; |
| 107 | |
| 108 | case 40: // 'Down' |
| 109 | e.halt(); |
| 110 | t.next(); |
| 111 | break; |
| 112 | |
| 113 | case 34: // 'Page down' |
| 114 | e.halt(); |
| 115 | t.pageDown(); |
| 116 | break; |
| 117 | |
| 118 | case 39: // 'Right' |
| 119 | // "Use" this item |
| 120 | if (t._prefix.active()) |
| 121 | break; |
| 122 | |
| 123 | |
| 124 | else if (t._entry.active()){ |
| 125 | break; |
| 126 | }; |
| 127 | |
| 128 | const item = t.liveItem(t.position); |
| 129 | |
| 130 | if (item["further"] !== undefined) { |
| 131 | item["further"].bind(item).apply(); |
| 132 | }; |
| 133 | |
| 134 | e.halt(); |
| 135 | break; |
| 136 | |
| 137 | case 13: // 'Enter' |
| 138 | // Click on prefix |
| 139 | if (t._prefix.active()) |
| 140 | t._prefix.onclick(e); |
| 141 | //Click on entry |
| 142 | else if (t._entry.active()) |
| 143 | t._entry.onclick(e); |
| 144 | // Click on item |
| 145 | else |
| 146 | t.liveItem(t.position).onclick(e); |
| 147 | e.halt(); |
| 148 | break; |
| 149 | |
| 150 | case 8: // 'Backspace' |
| 151 | t._prefix.chop(); |
| 152 | t._entry.chop(); |
| 153 | t.show(); |
| 154 | e.halt(); |
| 155 | break; |
| 156 | }; |
| 157 | }, |
| 158 | |
| 159 | // Add characters to prefix |
| 160 | _keypress : function (e) { |
| 161 | if (e.charCode !== 0) { |
| 162 | e.halt(); |
| 163 | |
| 164 | // Add prefix |
| 165 | this._prefix.add( |
| 166 | String.fromCharCode(_codeFromEvent(e)) |
| 167 | ); |
| 168 | this._entry.add( |
| 169 | String.fromCharCode(_codeFromEvent(e)) |
| 170 | ); |
| 171 | |
| 172 | this.show(); |
| 173 | }; |
| 174 | }, |
| 175 | |
| 176 | /** |
| 177 | * Filter the list and make it visible. |
| 178 | * This is always called once the prefix changes. |
| 179 | * |
| 180 | * @param {string} Prefix for filtering the list |
| 181 | */ |
| 182 | show : function (active) { |
| 183 | //only two new lines compared to menu.js show method (see NEW LINE) |
| 184 | const t = this; |
| 185 | |
| 186 | // show menu based on initial offset |
| 187 | t._unmark(); // Unmark everything that was marked before |
| 188 | t.removeItems(); |
| 189 | |
| 190 | // Initialize the list |
| 191 | if (!t._initList()) { |
| 192 | |
| 193 | // The prefix is not active |
| 194 | t._prefix.active(true); |
| 195 | //FIRST NEW LINE |
| 196 | t._entry.active(false); |
| 197 | |
| 198 | // finally show the element |
| 199 | t._el.classList.add('visible'); |
| 200 | |
| 201 | return true; |
| 202 | }; |
| 203 | |
| 204 | let offset = 0; |
| 205 | |
| 206 | // Set a chosen value to active and move the viewport |
| 207 | if (arguments.length === 1) { |
| 208 | |
| 209 | // Normalize active value |
| 210 | if (active < 0) { |
| 211 | active = 0; |
| 212 | } |
| 213 | else if (active >= t.liveLength()) { |
| 214 | active = t.liveLength() - 1; |
| 215 | }; |
| 216 | |
| 217 | // Item is outside the first viewport |
| 218 | if (active >= t._limit) { |
| 219 | offset = active; |
| 220 | const newOffset = t.liveLength() - t._limit; |
| 221 | if (offset > newOffset) { |
| 222 | offset = newOffset; |
| 223 | }; |
| 224 | }; |
| 225 | |
| 226 | t.position = active; |
| 227 | } |
| 228 | |
| 229 | // Choose the first item |
| 230 | else if (t._firstActive) { |
| 231 | t.position = 0; |
| 232 | } |
| 233 | |
| 234 | // Choose no item |
| 235 | else { |
| 236 | t.position = -1; |
| 237 | }; |
| 238 | |
| 239 | t.offset = offset; |
| 240 | t._showItems(offset); // Show new item list |
| 241 | |
| 242 | // Make chosen value active |
| 243 | if (t.position !== -1) { |
| 244 | t.liveItem(t.position).active(true); |
| 245 | }; |
| 246 | |
| 247 | // The prefix is not active |
| 248 | t._prefix.active(false); |
| 249 | //SECOND NEW LINE |
| 250 | t._entry.active(false); |
| 251 | |
| 252 | // finally show the element |
| 253 | t._el.classList.add('visible'); |
| 254 | |
| 255 | // Add classes for rolling menus |
| 256 | t._boundary(true); |
| 257 | |
| 258 | return true; |
| 259 | }, |
| 260 | |
| 261 | /** |
| 262 | * Hide the menu and call the onHide callback. |
| 263 | */ |
| 264 | hide : function () { |
| 265 | if (!this.dontHide) { |
| 266 | this.removeItems(); |
| 267 | this._prefix.clear(); |
| 268 | this._entry.clear(); |
| 269 | this.onHide(); |
| 270 | this._el.classList.remove('visible'); |
| 271 | } |
| 272 | // this._el.blur(); |
| 273 | }, |
| 274 | |
| 275 | |
| 276 | /** |
| 277 | * The alwaysEntry object |
| 278 | * the menu is attached to. |
| 279 | */ |
| 280 | alwaysEntry : function () { |
| 281 | return this._entry; |
| 282 | }, |
| 283 | |
| 284 | /** |
| 285 | * Get/Set the alwaysEntry Text |
| 286 | */ |
| 287 | alwaysEntryValue : function (value) { |
| 288 | if (arguments.length === 1) { |
| 289 | this._entry.value(value); |
| 290 | return this; |
| 291 | }; |
| 292 | return this._entry.value(); |
| 293 | }, |
| 294 | |
| 295 | /** |
| 296 | * Delete all visible items from the menu element |
| 297 | */ |
| 298 | |
| 299 | /** |
| 300 | * Make the next item in the filtered menu active |
| 301 | */ |
| 302 | next : function () { |
| 303 | //Hohe zyklomatische Komplexität |
| 304 | const t = this; |
| 305 | // Activate prefix and entry |
| 306 | const prefix = this._prefix; |
| 307 | const entry = this._entry; |
| 308 | |
| 309 | // No list |
| 310 | if (t.liveLength() === 0){ //switch between entry and prefix |
| 311 | if (!prefix.isSet()){//It is entry and it will stay entry |
| 312 | entry.active(true); |
| 313 | prefix.active(false);//Question: do we need to activate entry? |
| 314 | return; |
| 315 | }; |
| 316 | if (prefix.active() && !entry.active()){ |
| 317 | t.position = 2; // ? |
| 318 | prefix.active(false); |
| 319 | entry.active(true); //activate entry |
| 320 | return; |
| 321 | } |
| 322 | else if (!prefix.active() && entry.active()){ |
| 323 | t.position = 1; // ? |
| 324 | prefix.active(true); //activate prefix |
| 325 | entry.active(false); |
| 326 | return; |
| 327 | }; |
| 328 | //otherwise: confusion |
| 329 | return; |
| 330 | }; |
| 331 | |
| 332 | // liveLength!=0 |
| 333 | // Deactivate old item |
| 334 | if (t.position !== -1 && !t._prefix.active() && !t._entry.active()) { |
| 335 | t.liveItem(t.position).active(false); |
| 336 | }; |
| 337 | |
| 338 | // Get new active item |
| 339 | t.position++; |
| 340 | let newItem = t.liveItem(t.position); |
| 341 | |
| 342 | // The next element is undefined - roll to top or to prefix or to entry |
| 343 | if (newItem === undefined) { |
| 344 | |
| 345 | if ( !entry.active() ){ //if entry is active we definetly go to first item next |
| 346 | if (prefix.isSet() && !prefix.active()){ //prefix is next and exists |
| 347 | t.position=t.liveLength()+1; |
| 348 | prefix.active(true); //activate prefix |
| 349 | entry.active(false); |
| 350 | return; |
| 351 | } |
| 352 | else if ( (prefix.isSet() && prefix.active()) || // we had prefix |
| 353 | (!prefix.isSet() && !prefix.active()) ){ //or it isnt there |
| 354 | t.position=t.liveLength()+2; |
| 355 | prefix.active(false); |
| 356 | entry.active(true); //activate entry |
| 357 | return; |
| 358 | }; |
| 359 | } |
| 360 | |
| 361 | // Choose first item |
| 362 | else { |
| 363 | newItem = t.liveItem(0); |
| 364 | // choose first item |
| 365 | t.position = 0; |
| 366 | t._showItems(0); |
| 367 | // we reach point A from here |
| 368 | }; |
| 369 | } |
| 370 | |
| 371 | // The next element is after the viewport - roll down |
| 372 | else if (t.position >= (t.limit() + t.offset)) { |
| 373 | t.screen(t.position - t.limit() + 1); |
| 374 | } |
| 375 | |
| 376 | // The next element is before the viewport - roll up |
| 377 | else if (t.position <= t.offset) { |
| 378 | t.screen(t.position); |
| 379 | }; |
| 380 | |
| 381 | //Point A |
| 382 | t._prefix.active(false); |
| 383 | t._entry.active(false); |
| 384 | newItem.active(true); |
| 385 | }, |
| 386 | |
| 387 | |
| 388 | /* |
| 389 | * Make the previous item in the menu active |
| 390 | */ |
| 391 | prev : function () { |
| 392 | const t = this; |
| 393 | // Activate prefix and entry |
| 394 | const prefix = this._prefix; |
| 395 | const entry = this._entry; |
| 396 | |
| 397 | // No list |
| 398 | if (t.liveLength() === 0){ //switch between entry and prefix |
| 399 | if (!prefix.isSet()){//It is entry and it will stay entry |
| 400 | entry.active(true); |
| 401 | prefix.active(false);//Question: do we need to activate entry? |
| 402 | return; |
| 403 | }; |
| 404 | |
| 405 | if (prefix.active() && !entry.active()){ |
| 406 | t.position = 2; // ? |
| 407 | prefix.active(false); |
| 408 | entry.active(true); //activate entry |
| 409 | return; |
| 410 | } |
| 411 | else if (!prefix.active() && entry.active()){ |
| 412 | t.position = 1; // ? |
| 413 | prefix.active(true); //activate prefix |
| 414 | entry.active(false); |
| 415 | return; |
| 416 | }; |
| 417 | //otherwise: confusion |
| 418 | }; |
| 419 | |
| 420 | // Deactivate old item |
| 421 | if (!prefix.active() && !entry.active()) { |
| 422 | |
| 423 | // No active element set |
| 424 | if (t.position === -1) { |
| 425 | t.position = t.liveLength(); |
| 426 | } |
| 427 | |
| 428 | // deactivate active element |
| 429 | else { |
| 430 | t.liveItem(t.position--).active(false); //returns before decrement |
| 431 | }; |
| 432 | }; |
| 433 | |
| 434 | // Get new active item |
| 435 | let newItem = t.liveItem(t.position); |
| 436 | |
| 437 | // The previous element is undefined - roll to bottom |
| 438 | if (newItem === undefined) { |
| 439 | |
| 440 | |
| 441 | let offset = t.liveLength() - t.limit(); |
| 442 | |
| 443 | // Normalize offset |
| 444 | offset = offset < 0 ? 0 : offset; |
| 445 | |
| 446 | // Choose the last item |
| 447 | t.position = t.liveLength() - 1; |
| 448 | |
| 449 | if(!entry.active()){ |
| 450 | if (prefix.isSet() && prefix.active()){ |
| 451 | // we were on prefix and now choose last item |
| 452 | newItem = t.liveItem(t.position); |
| 453 | t._showItems(offset); |
| 454 | } |
| 455 | else if(!prefix.active()){ |
| 456 | // we need to loop around: pick entry |
| 457 | t.position=t.liveLength()+2; |
| 458 | prefix.active(false); |
| 459 | entry.active(true); //activate entry |
| 460 | return; |
| 461 | }; |
| 462 | //otherwise confusion |
| 463 | } else { |
| 464 | if(prefix.isSet()){ // we had entry and thus now need prefix |
| 465 | t.position=t.liveLength()+1; |
| 466 | prefix.active(true); //activate prefix |
| 467 | entry.active(false); |
| 468 | return; |
| 469 | } else { // we had entry but there is no prefix |
| 470 | newItem = t.liveItem(t.position); |
| 471 | t._showItems(offset); // Choose last item |
| 472 | }; |
| 473 | }; |
| 474 | } |
| 475 | |
| 476 | // The previous element is before the view - roll up |
| 477 | else if (t.position < t.offset) { |
| 478 | t.screen(t.position); |
| 479 | } |
| 480 | |
| 481 | // The previous element is after the view - roll down |
| 482 | else if (t.position >= (t.limit() + t.offset)) { |
| 483 | t.screen(t.position - t.limit() + 2); |
| 484 | }; |
| 485 | |
| 486 | t._prefix.active(false); |
| 487 | t._entry.active(false); |
| 488 | newItem.active(true); |
| 489 | }, |
| 490 | // Append Items that should be shown |
| 491 | _showItems : function (off) { |
| 492 | const t = this; |
| 493 | |
| 494 | // optimization: scroll down one step |
| 495 | if (t.offset === (off - 1)) { |
| 496 | t.offset = off; |
| 497 | |
| 498 | // Remove the HTML node from the first item |
| 499 | // leave lengthField/prefix/slider |
| 500 | t._el.removeChild(t._el.children[this._notItemElements]); |
| 501 | |
| 502 | t._append( |
| 503 | t._list[t.offset + t.limit() - 1] |
| 504 | ); |
| 505 | } |
| 506 | |
| 507 | // optimization: scroll up one step |
| 508 | else if (t.offset === (off + 1)) { |
| 509 | t.offset = off; |
| 510 | |
| 511 | // Remove the HTML node from the last item |
| 512 | t._el.removeChild(t._el.lastChild); |
| 513 | |
| 514 | t._prepend(t._list[t.offset]); |
| 515 | } |
| 516 | |
| 517 | else { |
| 518 | t.offset = off; |
| 519 | |
| 520 | // Remove all items |
| 521 | t.removeItems(); |
| 522 | |
| 523 | // Use list |
| 524 | let shown = 0; |
| 525 | |
| 526 | for (let i = 0; i < t._list.length; i++) { |
| 527 | |
| 528 | // Don't show - it's before offset |
| 529 | shown++; |
| 530 | if (shown <= off) |
| 531 | continue; |
| 532 | |
| 533 | t._append(t._list[i]); |
| 534 | |
| 535 | if (shown >= (t.limit() + off)) |
| 536 | break; |
| 537 | }; |
| 538 | }; |
| 539 | |
| 540 | // set the slider to the new offset |
| 541 | t._slider.offset(t.offset); |
| 542 | }, |
| 543 | |
| 544 | |
| 545 | }; |
| 546 | }); |