New menu class that has an entry at the very end, similar to the input
text prefix, that is always available
Change-Id: I03d8f689e37021d3daac6bf34f7d35f0e4d71999
diff --git a/dev/js/src/alwaysmenu.js b/dev/js/src/alwaysmenu.js
new file mode 100644
index 0000000..4bcf9da
--- /dev/null
+++ b/dev/js/src/alwaysmenu.js
@@ -0,0 +1,546 @@
+/**
+ *
+ * A Version of the menu class, that
+ * has an always displayed entry that can be
+ * clicked and contains text
+ *
+ * This entry button may or may not be displayed on top of objects
+ * lying under (>y) this menu. See alwaysentry update: negative absolute
+ * y coordinate.
+ *
+ * @author Leo Repp
+ */
+
+"use strict";
+define([
+ 'menu',
+ 'menu/item',
+ 'menu/prefix',
+ 'menu/lengthField',
+ 'alwaysentry',
+ 'util'
+], function (
+ menuClass,
+ defaultItemClass,
+ defaultPrefixClass,
+ defaultLengthFieldClass,
+ defaultAlwaysEntryClass) {
+
+ return {
+
+ /**
+ * Create new menu with an always visible entry.
+ * @this {AlwaysMenu}
+ * @constructor
+ * @param {Object["like this"]} params Object with attributes prefixCLass, itemClass, lengthFieldClass, alwaysEntryClass
+ * @param {Array.<Array.<string>>} list list of menu items
+ */
+ create : function (list, params) {
+ const obj = menuClass.create(list,params)
+ .upgradeTo(this)
+ ._init(list, params);
+
+ obj._el.classList.add('alwaysmenu');
+
+ //add entry object and allow for own entryClasses
+ if (params!==undefined && params["alwaysEntryClass"] !== undefined) {
+ obj._entry = params["alwaysEntryClass"].create();
+ } else {
+ obj._entry=defaultAlwaysEntryClass.create();
+ }
+ obj._entry._menu=obj;
+ obj._notItemElements=4;
+ // add entry to HTML element
+ obj._el.appendChild(obj._entry.element());
+
+ return obj;
+ },
+
+
+ /**
+ * Destroy this menu
+ * (in case you don't trust the
+ * mark and sweep GC)!
+ */
+ destroy : function () {
+ //based on menu.js
+ const t = this;
+
+ // Remove circular reference to "this" in menu
+ if (t._el != undefined)
+ delete t._el["menu"];
+
+ // Remove circular reference to "this" in items
+ t._items.forEach(function(i) {
+ delete i["_menu"];
+ });
+
+ // Remove circular reference to "this" in prefix
+ delete t._prefix['_menu'];
+ delete t._lengthField['_menu'];
+ delete t._slider['_menu'];
+ delete t._entry['_menu'];
+ },
+
+
+ // Arrow key and prefix treatment
+ _keydown : function (e) {
+ //based on menu.js
+ const t = this;
+
+ switch (_codeFromEvent(e)) {
+
+ case 27: // 'Esc'
+ e.halt();
+ t.hide();
+ break;
+
+ case 38: // 'Up'
+ e.halt();
+ t.prev();
+ break;
+
+ case 33: // 'Page up'
+ e.halt();
+ t.pageUp();
+ break;
+
+ case 40: // 'Down'
+ e.halt();
+ t.next();
+ break;
+
+ case 34: // 'Page down'
+ e.halt();
+ t.pageDown();
+ break;
+
+ case 39: // 'Right'
+ // "Use" this item
+ if (t._prefix.active())
+ break;
+
+
+ else if (t._entry.active()){
+ break;
+ };
+
+ const item = t.liveItem(t.position);
+
+ if (item["further"] !== undefined) {
+ item["further"].bind(item).apply();
+ };
+
+ e.halt();
+ break;
+
+ case 13: // 'Enter'
+ // Click on prefix
+ if (t._prefix.active())
+ t._prefix.onclick(e);
+ //Click on entry
+ else if (t._entry.active())
+ t._entry.onclick(e);
+ // Click on item
+ else
+ t.liveItem(t.position).onclick(e);
+ e.halt();
+ break;
+
+ case 8: // 'Backspace'
+ t._prefix.chop();
+ t._entry.chop();
+ t.show();
+ e.halt();
+ break;
+ };
+ },
+
+ // Add characters to prefix
+ _keypress : function (e) {
+ if (e.charCode !== 0) {
+ e.halt();
+
+ // Add prefix
+ this._prefix.add(
+ String.fromCharCode(_codeFromEvent(e))
+ );
+ this._entry.add(
+ String.fromCharCode(_codeFromEvent(e))
+ );
+
+ this.show();
+ };
+ },
+
+ /**
+ * Filter the list and make it visible.
+ * This is always called once the prefix changes.
+ *
+ * @param {string} Prefix for filtering the list
+ */
+ show : function (active) {
+ //only two new lines compared to menu.js show method (see NEW LINE)
+ const t = this;
+
+ // show menu based on initial offset
+ t._unmark(); // Unmark everything that was marked before
+ t.removeItems();
+
+ // Initialize the list
+ if (!t._initList()) {
+
+ // The prefix is not active
+ t._prefix.active(true);
+ //FIRST NEW LINE
+ t._entry.active(false);
+
+ // finally show the element
+ t._el.classList.add('visible');
+
+ return true;
+ };
+
+ let offset = 0;
+
+ // Set a chosen value to active and move the viewport
+ if (arguments.length === 1) {
+
+ // Normalize active value
+ if (active < 0) {
+ active = 0;
+ }
+ else if (active >= t.liveLength()) {
+ active = t.liveLength() - 1;
+ };
+
+ // Item is outside the first viewport
+ if (active >= t._limit) {
+ offset = active;
+ const newOffset = t.liveLength() - t._limit;
+ if (offset > newOffset) {
+ offset = newOffset;
+ };
+ };
+
+ t.position = active;
+ }
+
+ // Choose the first item
+ else if (t._firstActive) {
+ t.position = 0;
+ }
+
+ // Choose no item
+ else {
+ t.position = -1;
+ };
+
+ t.offset = offset;
+ t._showItems(offset); // Show new item list
+
+ // Make chosen value active
+ if (t.position !== -1) {
+ t.liveItem(t.position).active(true);
+ };
+
+ // The prefix is not active
+ t._prefix.active(false);
+ //SECOND NEW LINE
+ t._entry.active(false);
+
+ // finally show the element
+ t._el.classList.add('visible');
+
+ // Add classes for rolling menus
+ t._boundary(true);
+
+ return true;
+ },
+
+ /**
+ * Hide the menu and call the onHide callback.
+ */
+ hide : function () {
+ if (!this.dontHide) {
+ this.removeItems();
+ this._prefix.clear();
+ this._entry.clear();
+ this.onHide();
+ this._el.classList.remove('visible');
+ }
+ // this._el.blur();
+ },
+
+
+ /**
+ * The alwaysEntry object
+ * the menu is attached to.
+ */
+ alwaysEntry : function () {
+ return this._entry;
+ },
+
+ /**
+ * Get/Set the alwaysEntry Text
+ */
+ alwaysEntryValue : function (value) {
+ if (arguments.length === 1) {
+ this._entry.value(value);
+ return this;
+ };
+ return this._entry.value();
+ },
+
+ /**
+ * Delete all visible items from the menu element
+ */
+
+ /**
+ * Make the next item in the filtered menu active
+ */
+ next : function () {
+ //Hohe zyklomatische Komplexität
+ const t = this;
+ // Activate prefix and entry
+ const prefix = this._prefix;
+ const entry = this._entry;
+
+ // No list
+ if (t.liveLength() === 0){ //switch between entry and prefix
+ if (!prefix.isSet()){//It is entry and it will stay entry
+ entry.active(true);
+ prefix.active(false);//Question: do we need to activate entry?
+ return;
+ };
+ if (prefix.active() && !entry.active()){
+ t.position = 2; // ?
+ prefix.active(false);
+ entry.active(true); //activate entry
+ return;
+ }
+ else if (!prefix.active() && entry.active()){
+ t.position = 1; // ?
+ prefix.active(true); //activate prefix
+ entry.active(false);
+ return;
+ };
+ //otherwise: confusion
+ return;
+ };
+
+ // liveLength!=0
+ // Deactivate old item
+ if (t.position !== -1 && !t._prefix.active() && !t._entry.active()) {
+ t.liveItem(t.position).active(false);
+ };
+
+ // Get new active item
+ t.position++;
+ let newItem = t.liveItem(t.position);
+
+ // The next element is undefined - roll to top or to prefix or to entry
+ if (newItem === undefined) {
+
+ if ( !entry.active() ){ //if entry is active we definetly go to first item next
+ if (prefix.isSet() && !prefix.active()){ //prefix is next and exists
+ t.position=t.liveLength()+1;
+ prefix.active(true); //activate prefix
+ entry.active(false);
+ return;
+ }
+ else if ( (prefix.isSet() && prefix.active()) || // we had prefix
+ (!prefix.isSet() && !prefix.active()) ){ //or it isnt there
+ t.position=t.liveLength()+2;
+ prefix.active(false);
+ entry.active(true); //activate entry
+ return;
+ };
+ }
+
+ // Choose first item
+ else {
+ newItem = t.liveItem(0);
+ // choose first item
+ t.position = 0;
+ t._showItems(0);
+ // we reach point A from here
+ };
+ }
+
+ // The next element is after the viewport - roll down
+ else if (t.position >= (t.limit() + t.offset)) {
+ t.screen(t.position - t.limit() + 1);
+ }
+
+ // The next element is before the viewport - roll up
+ else if (t.position <= t.offset) {
+ t.screen(t.position);
+ };
+
+ //Point A
+ t._prefix.active(false);
+ t._entry.active(false);
+ newItem.active(true);
+ },
+
+
+ /*
+ * Make the previous item in the menu active
+ */
+ prev : function () {
+ const t = this;
+ // Activate prefix and entry
+ const prefix = this._prefix;
+ const entry = this._entry;
+
+ // No list
+ if (t.liveLength() === 0){ //switch between entry and prefix
+ if (!prefix.isSet()){//It is entry and it will stay entry
+ entry.active(true);
+ prefix.active(false);//Question: do we need to activate entry?
+ return;
+ };
+
+ if (prefix.active() && !entry.active()){
+ t.position = 2; // ?
+ prefix.active(false);
+ entry.active(true); //activate entry
+ return;
+ }
+ else if (!prefix.active() && entry.active()){
+ t.position = 1; // ?
+ prefix.active(true); //activate prefix
+ entry.active(false);
+ return;
+ };
+ //otherwise: confusion
+ };
+
+ // Deactivate old item
+ if (!prefix.active() && !entry.active()) {
+
+ // No active element set
+ if (t.position === -1) {
+ t.position = t.liveLength();
+ }
+
+ // deactivate active element
+ else {
+ t.liveItem(t.position--).active(false); //returns before decrement
+ };
+ };
+
+ // Get new active item
+ let newItem = t.liveItem(t.position);
+
+ // The previous element is undefined - roll to bottom
+ if (newItem === undefined) {
+
+
+ let offset = t.liveLength() - t.limit();
+
+ // Normalize offset
+ offset = offset < 0 ? 0 : offset;
+
+ // Choose the last item
+ t.position = t.liveLength() - 1;
+
+ if(!entry.active()){
+ if (prefix.isSet() && prefix.active()){
+ // we were on prefix and now choose last item
+ newItem = t.liveItem(t.position);
+ t._showItems(offset);
+ }
+ else if(!prefix.active()){
+ // we need to loop around: pick entry
+ t.position=t.liveLength()+2;
+ prefix.active(false);
+ entry.active(true); //activate entry
+ return;
+ };
+ //otherwise confusion
+ } else {
+ if(prefix.isSet()){ // we had entry and thus now need prefix
+ t.position=t.liveLength()+1;
+ prefix.active(true); //activate prefix
+ entry.active(false);
+ return;
+ } else { // we had entry but there is no prefix
+ newItem = t.liveItem(t.position);
+ t._showItems(offset); // Choose last item
+ };
+ };
+ }
+
+ // The previous element is before the view - roll up
+ else if (t.position < t.offset) {
+ t.screen(t.position);
+ }
+
+ // The previous element is after the view - roll down
+ else if (t.position >= (t.limit() + t.offset)) {
+ t.screen(t.position - t.limit() + 2);
+ };
+
+ t._prefix.active(false);
+ t._entry.active(false);
+ newItem.active(true);
+ },
+// Append Items that should be shown
+_showItems : function (off) {
+ const t = this;
+
+ // optimization: scroll down one step
+ if (t.offset === (off - 1)) {
+ t.offset = off;
+
+ // Remove the HTML node from the first item
+ // leave lengthField/prefix/slider
+ t._el.removeChild(t._el.children[this._notItemElements]);
+
+ t._append(
+ t._list[t.offset + t.limit() - 1]
+ );
+ }
+
+ // optimization: scroll up one step
+ else if (t.offset === (off + 1)) {
+ t.offset = off;
+
+ // Remove the HTML node from the last item
+ t._el.removeChild(t._el.lastChild);
+
+ t._prepend(t._list[t.offset]);
+ }
+
+ else {
+ t.offset = off;
+
+ // Remove all items
+ t.removeItems();
+
+ // Use list
+ let shown = 0;
+
+ for (let i = 0; i < t._list.length; i++) {
+
+ // Don't show - it's before offset
+ shown++;
+ if (shown <= off)
+ continue;
+
+ t._append(t._list[i]);
+
+ if (shown >= (t.limit() + off))
+ break;
+ };
+ };
+
+ // set the slider to the new offset
+ t._slider.offset(t.offset);
+},
+
+
+ };
+});