/* jslint node: true */ 'use strict'; // ENiGMA½ const View = require('./view.js').View; const miscUtil = require('./misc_util.js'); const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps const util = require('util'); const assert = require('assert'); const _ = require('lodash'); exports.MenuView = MenuView; function MenuView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); View.call(this, options); this.disablePipe = options.disablePipe || false; const self = this; if(options.items) { this.setItems(options.items); } else { this.items = []; } this.renderCache = {}; this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); this.setHotKeys(options.hotKeys); this.focusedItemIndex = options.focusedItemIndex || 0; this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization this.focusPrefix = options.focusPrefix || ''; this.focusSuffix = options.focusSuffix || ''; this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); this.hasFocusItems = function() { return !_.isUndefined(self.focusItems); }; this.getHotKeyItemIndex = function(ch) { if(ch && self.hotKeys) { const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; if(_.isNumber(keyIndex)) { return keyIndex; } } return -1; }; this.emitIndexUpdate = function() { self.emit('index update', self.focusedItemIndex); }; } util.inherits(MenuView, View); MenuView.prototype.setTextOverflow = function(overflow) { this.textOverflow = overflow; this.invalidateRenderCache(); } MenuView.prototype.hasTextOverflow = function() { return this.textOverflow != undefined; } MenuView.prototype.setItems = function(items) { if(Array.isArray(items)) { this.sorted = false; this.renderCache = {}; // // Items can be an array of strings or an array of objects. // // In the case of objects, items are considered complex and // may have one or more members that can later be formatted // against. The default member is 'text'. The member 'data' // may be overridden to provide a form value other than the // item's index. // // Items can be formatted with 'itemFormat' and 'focusItemFormat' // let text; let stringItem; this.items = items.map(item => { stringItem = _.isString(item); if(stringItem) { text = item; } else { text = item.text || ''; this.complexItems = true; } text = this.disablePipe ? text : pipeToAnsi(text, this.client); return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others }); if(this.complexItems) { this.itemFormat = this.itemFormat || '{text}'; } this.invalidateRenderCache(); } }; MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { const item = this.renderCache[index]; return item && item[focusItem ? 'focus' : 'standard']; }; MenuView.prototype.removeRenderCacheItem = function(index) { delete this.renderCache[index]; }; MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { this.renderCache[index] = this.renderCache[index] || {}; this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; }; MenuView.prototype.invalidateRenderCache = function() { this.renderCache = {}; }; MenuView.prototype.setSort = function(sort) { if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { return; } const key = true === sort ? 'text' : sort; if('text' !== sort && !this.complexItems) { return; // need a valid sort key } this.items.sort( (a, b) => { const a1 = a[key]; const b1 = b[key]; if(!a1) { return -1; } if(!b1) { return 1; } return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); }); this.sorted = true; }; MenuView.prototype.removeItem = function(index) { this.sorted = false; this.items.splice(index, 1); if(this.focusItems) { this.focusItems.splice(index, 1); } if(this.focusedItemIndex >= index) { this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); } this.removeRenderCacheItem(index); this.positionCacheExpired = true; }; MenuView.prototype.getCount = function() { return this.items.length; }; MenuView.prototype.getItems = function() { if(this.complexItems) { return this.items; } return this.items.map( item => { return item.text; }); }; MenuView.prototype.getItem = function(index) { if(this.complexItems) { return this.items[index]; } return this.items[index].text; }; MenuView.prototype.focusNext = function() { this.emitIndexUpdate(); }; MenuView.prototype.focusPrevious = function() { this.emitIndexUpdate(); }; MenuView.prototype.focusNextPageItem = function() { this.emitIndexUpdate(); }; MenuView.prototype.focusPreviousPageItem = function() { this.emitIndexUpdate(); }; MenuView.prototype.focusFirst = function() { this.emitIndexUpdate(); }; MenuView.prototype.focusLast = function() { this.emitIndexUpdate(); }; MenuView.prototype.setFocusItemIndex = function(index) { this.focusedItemIndex = index; }; MenuView.prototype.onKeyPress = function(ch, key) { const itemIndex = this.getHotKeyItemIndex(ch); if(itemIndex >= 0) { this.setFocusItemIndex(itemIndex); if(true === this.hotKeySubmit) { this.emit('action', 'accept'); } } MenuView.super_.prototype.onKeyPress.call(this, ch, key); }; MenuView.prototype.setFocusItems = function(items) { const self = this; if(items) { this.focusItems = []; items.forEach( itemText => { this.focusItems.push( { text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) } ); }); } }; MenuView.prototype.setItemSpacing = function(itemSpacing) { itemSpacing = parseInt(itemSpacing); assert(_.isNumber(itemSpacing)); this.itemSpacing = itemSpacing; this.positionCacheExpired = true; }; MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { itemHorizSpacing = parseInt(itemHorizSpacing); assert(_.isNumber(itemHorizSpacing)); this.itemHorizSpacing = itemHorizSpacing; this.positionCacheExpired = true; }; MenuView.prototype.setPropertyValue = function(propName, value) { switch(propName) { case 'itemSpacing' : this.setItemSpacing(value); break; case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break; case 'items' : this.setItems(value); break; case 'focusItems' : this.setFocusItems(value); break; case 'hotKeys' : this.setHotKeys(value); break; case 'textOverflow' : this.setTextOverflow(value); break; case 'hotKeySubmit' : this.hotKeySubmit = value; break; case 'justify' : this.setJustify(value); break; case 'fillChar' : this.setFillChar(value); break; case 'focusItemIndex' : this.focusedItemIndex = value; break; case 'itemFormat' : case 'focusItemFormat' : this[propName] = value; // if there is a cache currently, invalidate it this.invalidateRenderCache(); break; case 'sort' : this.setSort(value); break; } MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; MenuView.prototype.setFillChar = function(fillChar) { this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1); this.invalidateRenderCache(); } MenuView.prototype.setJustify = function(justify) { this.justify = justify; this.invalidateRenderCache(); this.positionCacheExpired = true; } MenuView.prototype.setHotKeys = function(hotKeys) { if(_.isObject(hotKeys)) { if(this.caseInsensitiveHotKeys) { this.hotKeys = {}; for(var key in hotKeys) { this.hotKeys[key.toLowerCase()] = hotKeys[key]; } } else { this.hotKeys = hotKeys; } } };