2014-10-23 05:41:00 +00:00
|
|
|
/* jslint node: true */
|
|
|
|
'use strict';
|
|
|
|
|
2015-03-30 03:47:48 +00:00
|
|
|
// ENiGMA½
|
2014-10-23 05:41:00 +00:00
|
|
|
var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
|
2015-03-28 00:02:00 +00:00
|
|
|
var menuUtil = require('./menu_util.js');
|
2015-04-05 07:15:04 +00:00
|
|
|
var asset = require('./asset.js');
|
2015-04-28 04:40:05 +00:00
|
|
|
var ansi = require('./ansi_term.js');
|
2015-03-28 00:02:00 +00:00
|
|
|
|
2016-07-25 20:35:58 +00:00
|
|
|
// deps
|
2015-03-30 03:47:48 +00:00
|
|
|
var events = require('events');
|
|
|
|
var util = require('util');
|
|
|
|
var assert = require('assert');
|
2015-03-28 00:02:00 +00:00
|
|
|
var async = require('async');
|
2015-04-02 04:13:29 +00:00
|
|
|
var _ = require('lodash');
|
2015-04-19 08:13:13 +00:00
|
|
|
var paths = require('path');
|
2014-10-23 05:41:00 +00:00
|
|
|
|
|
|
|
exports.ViewController = ViewController;
|
|
|
|
|
2015-04-19 08:13:13 +00:00
|
|
|
var MCI_REGEXP = /([A-Z]{2})([0-9]{1,2})/;
|
|
|
|
|
2015-04-14 06:19:14 +00:00
|
|
|
function ViewController(options) {
|
|
|
|
assert(_.isObject(options));
|
|
|
|
assert(_.isObject(options.client));
|
2015-04-21 04:50:58 +00:00
|
|
|
|
2014-10-23 05:41:00 +00:00
|
|
|
events.EventEmitter.call(this);
|
|
|
|
|
2015-04-20 04:58:18 +00:00
|
|
|
var self = this;
|
2014-10-23 05:41:00 +00:00
|
|
|
|
2015-04-20 04:58:18 +00:00
|
|
|
this.client = options.client;
|
|
|
|
this.views = {}; // map of ID -> view
|
|
|
|
this.formId = options.formId || 0;
|
2015-07-11 22:39:42 +00:00
|
|
|
this.mciViewFactory = new MCIViewFactory(this.client); // :TODO: can this not be a singleton?
|
2015-07-25 22:10:12 +00:00
|
|
|
this.noInput = _.isBoolean(options.noInput) ? options.noInput : false;
|
2015-07-10 05:23:37 +00:00
|
|
|
|
|
|
|
this.actionKeyMap = {};
|
2014-10-23 05:41:00 +00:00
|
|
|
|
2016-07-25 20:35:58 +00:00
|
|
|
//
|
|
|
|
// Small wrapper/proxy around handleAction() to ensure we do not allow
|
|
|
|
// input/additional actions queued while performing an action
|
|
|
|
//
|
|
|
|
this.handleActionWrapper = function(formData, actionBlock) {
|
|
|
|
if(self.waitActionCompletion) {
|
|
|
|
return; // ignore until this is finished!
|
|
|
|
}
|
|
|
|
|
|
|
|
self.waitActionCompletion = true;
|
|
|
|
menuUtil.handleAction(self.client, formData, actionBlock, (err) => {
|
|
|
|
if(err) {
|
|
|
|
// :TODO: What can we really do here?
|
2016-09-20 03:30:26 +00:00
|
|
|
if('ALREADYTHERE' === err.reasonCode) {
|
|
|
|
self.client.log.trace( err.reason );
|
|
|
|
} else {
|
|
|
|
self.client.log.warn( { err : err }, 'Error during handleAction()');
|
|
|
|
}
|
2016-07-25 20:35:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
self.waitActionCompletion = false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-06-05 22:20:26 +00:00
|
|
|
this.clientKeyPressHandler = function(ch, key) {
|
|
|
|
//
|
2015-07-09 04:34:40 +00:00
|
|
|
// Process key presses treating form submit mapped keys special.
|
|
|
|
// Everything else is forwarded on to the focused View, if any.
|
2015-06-26 04:34:33 +00:00
|
|
|
//
|
2015-09-20 04:55:09 +00:00
|
|
|
var actionForKey = key ? self.actionKeyMap[key.name] : self.actionKeyMap[ch];
|
|
|
|
if(actionForKey) {
|
|
|
|
if(_.isNumber(actionForKey.viewId)) {
|
|
|
|
//
|
|
|
|
// Key works on behalf of a view -- switch focus & submit
|
|
|
|
//
|
|
|
|
self.switchFocus(actionForKey.viewId);
|
|
|
|
self.submitForm(key);
|
|
|
|
} else if(_.isString(actionForKey.action)) {
|
2016-07-25 20:35:58 +00:00
|
|
|
self.handleActionWrapper(
|
|
|
|
{ ch : ch, key : key }, // formData
|
|
|
|
actionForKey); // actionBlock
|
2015-07-10 05:23:37 +00:00
|
|
|
}
|
2015-09-21 01:10:09 +00:00
|
|
|
} else {
|
|
|
|
if(self.focusedView && self.focusedView.acceptsInput) {
|
|
|
|
self.focusedView.onKeyPress(ch, key);
|
|
|
|
}
|
2015-06-05 04:29:14 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-08-13 22:05:17 +00:00
|
|
|
this.viewActionListener = function(action, key) {
|
2014-10-23 05:41:00 +00:00
|
|
|
switch(action) {
|
2016-08-11 04:48:13 +00:00
|
|
|
case 'next' :
|
|
|
|
self.emit('action', { view : this, action : action, key : key });
|
2014-10-23 05:41:00 +00:00
|
|
|
self.nextFocus();
|
2016-08-11 04:48:13 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'accept' :
|
|
|
|
if(self.focusedView && self.focusedView.submit) {
|
|
|
|
// :TODO: need to do validation here!!!
|
|
|
|
var focusedView = self.focusedView;
|
|
|
|
self.validateView(focusedView, function validated(err, newFocusedViewId) {
|
|
|
|
if(err) {
|
|
|
|
var newFocusedView = self.getView(newFocusedViewId) || focusedView;
|
|
|
|
self.setViewFocusWithEvents(newFocusedView, true);
|
|
|
|
} else {
|
|
|
|
self.submitForm(key);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
//self.submitForm(key);
|
|
|
|
} else {
|
|
|
|
self.nextFocus();
|
|
|
|
}
|
|
|
|
break;
|
2014-10-23 05:41:00 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-08-13 22:05:17 +00:00
|
|
|
this.submitForm = function(key) {
|
|
|
|
self.emit('submit', this.getFormData(key));
|
2014-11-02 19:07:17 +00:00
|
|
|
};
|
|
|
|
|
2015-04-20 04:58:18 +00:00
|
|
|
this.getLogFriendlyFormData = function(formData) {
|
2015-04-27 02:46:16 +00:00
|
|
|
// :TODO: these fields should be part of menu.json sensitiveMembers[]
|
2015-04-20 04:58:18 +00:00
|
|
|
var safeFormData = _.cloneDeep(formData);
|
|
|
|
if(safeFormData.value.password) {
|
|
|
|
safeFormData.value.password = '*****';
|
|
|
|
}
|
2015-04-24 05:00:48 +00:00
|
|
|
if(safeFormData.value.passwordConfirm) {
|
|
|
|
safeFormData.value.passwordConfirm = '*****';
|
|
|
|
}
|
2015-04-20 04:58:18 +00:00
|
|
|
return safeFormData;
|
|
|
|
};
|
|
|
|
|
2015-04-14 06:19:14 +00:00
|
|
|
this.switchFocusEvent = function(event, view) {
|
|
|
|
if(self.emitSwitchFocus) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.emitSwitchFocus = true;
|
|
|
|
self.emit(event, view);
|
|
|
|
self.emitSwitchFocus = false;
|
|
|
|
};
|
|
|
|
|
2015-04-20 04:58:18 +00:00
|
|
|
this.createViewsFromMCI = function(mciMap, cb) {
|
|
|
|
async.each(Object.keys(mciMap), function entry(name, nextItem) {
|
|
|
|
var mci = mciMap[name];
|
|
|
|
var view = self.mciViewFactory.createFromMCI(mci);
|
|
|
|
|
2016-10-13 04:07:22 +00:00
|
|
|
if(view) {
|
|
|
|
if(false === self.noInput) {
|
|
|
|
view.on('action', self.viewActionListener);
|
|
|
|
}
|
2015-04-20 04:58:18 +00:00
|
|
|
|
|
|
|
self.addView(view);
|
|
|
|
}
|
|
|
|
|
|
|
|
nextItem(null);
|
|
|
|
},
|
|
|
|
function complete(err) {
|
|
|
|
self.setViewOrder();
|
|
|
|
cb(err);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-04-28 04:40:05 +00:00
|
|
|
// :TODO: move this elsewhere
|
2015-04-20 04:58:18 +00:00
|
|
|
this.setViewPropertiesFromMCIConf = function(view, conf) {
|
2015-07-27 04:51:06 +00:00
|
|
|
|
|
|
|
var propAsset;
|
|
|
|
var propValue;
|
|
|
|
|
|
|
|
function callModuleMethod(path) {
|
|
|
|
if('' === paths.extname(path)) {
|
|
|
|
path += '.js';
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
var methodMod = require(path);
|
|
|
|
// :TODO: fix formData & extraArgs
|
|
|
|
return methodMod[propAsset.asset](self.client.currentMenuModule, {}, {} );
|
|
|
|
} catch(e) {
|
|
|
|
self.client.log.error( { error : e.toString(), methodName : propAsset.asset }, 'Failed to execute asset method');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-30 05:14:17 +00:00
|
|
|
for(var propName in conf) {
|
2015-07-27 04:51:06 +00:00
|
|
|
propAsset = asset.getViewPropertyAsset(conf[propName]);
|
2015-06-30 05:14:17 +00:00
|
|
|
if(propAsset) {
|
|
|
|
switch(propAsset.type) {
|
2016-07-25 20:35:58 +00:00
|
|
|
case 'config' :
|
|
|
|
propValue = asset.resolveConfigAsset(conf[propName]);
|
|
|
|
break;
|
2016-08-04 03:46:38 +00:00
|
|
|
|
|
|
|
case 'sysStat' :
|
|
|
|
propValue = asset.resolveSystemStatAsset(conf[propName]);
|
|
|
|
break;
|
2016-07-25 20:35:58 +00:00
|
|
|
|
|
|
|
// :TODO: handle @art (e.g. text : @art ...)
|
|
|
|
|
|
|
|
case 'method' :
|
|
|
|
case 'systemMethod' :
|
|
|
|
if('validate' === propName) {
|
|
|
|
// :TODO: handle propAsset.location for @method script specification
|
|
|
|
if('systemMethod' === propAsset.type) {
|
|
|
|
// :TODO: implementation validation @systemMethod handling!
|
|
|
|
var methodModule = require(paths.join(__dirname, 'system_view_validate.js'));
|
|
|
|
if(_.isFunction(methodModule[propAsset.asset])) {
|
|
|
|
propValue = methodModule[propAsset.asset];
|
2015-12-10 07:04:38 +00:00
|
|
|
}
|
|
|
|
} else {
|
2016-07-25 20:35:58 +00:00
|
|
|
if(_.isFunction(self.client.currentMenuModule.menuMethods[propAsset.asset])) {
|
|
|
|
propValue = self.client.currentMenuModule.menuMethods[propAsset.asset];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if(_.isString(propAsset.location)) {
|
2015-12-10 07:04:38 +00:00
|
|
|
|
2016-07-25 20:35:58 +00:00
|
|
|
} else {
|
|
|
|
if('systemMethod' === propAsset.type) {
|
|
|
|
// :TODO:
|
2015-07-27 04:51:06 +00:00
|
|
|
} else {
|
2016-07-25 20:35:58 +00:00
|
|
|
// local to current module
|
|
|
|
var currentModule = self.client.currentMenuModule;
|
|
|
|
if(_.isFunction(currentModule.menuMethods[propAsset.asset])) {
|
|
|
|
// :TODO: Fix formData & extraArgs... this all needs general processing
|
|
|
|
propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs);
|
2015-07-27 04:51:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-07-25 20:35:58 +00:00
|
|
|
}
|
|
|
|
break;
|
2015-07-27 04:51:06 +00:00
|
|
|
|
2016-07-25 20:35:58 +00:00
|
|
|
default :
|
|
|
|
propValue = propValue = conf[propName];
|
|
|
|
break;
|
2015-06-30 05:14:17 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
propValue = conf[propName];
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!_.isUndefined(propValue)) {
|
|
|
|
view.setPropertyValue(propName, propValue);
|
|
|
|
}
|
2015-07-04 18:02:37 +00:00
|
|
|
}
|
2015-04-20 04:58:18 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
this.applyViewConfig = function(config, cb) {
|
|
|
|
var highestId = 1;
|
|
|
|
var submitId;
|
|
|
|
var initialFocusId = 1;
|
|
|
|
|
2015-09-28 01:33:25 +00:00
|
|
|
async.each(Object.keys(config.mci || {}), function entry(mci, nextItem) {
|
2015-09-15 04:40:00 +00:00
|
|
|
var mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs????
|
|
|
|
if(null === mciMatch) {
|
|
|
|
self.client.log.warn( { mci : mci }, 'Unable to parse MCI code');
|
|
|
|
return;
|
|
|
|
}
|
2015-04-20 04:58:18 +00:00
|
|
|
|
2015-09-15 04:40:00 +00:00
|
|
|
var viewId = parseInt(mciMatch[2]);
|
|
|
|
assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used
|
2015-04-20 04:58:18 +00:00
|
|
|
|
2015-07-10 17:11:08 +00:00
|
|
|
if(viewId > highestId) {
|
|
|
|
highestId = viewId;
|
|
|
|
}
|
|
|
|
|
2015-10-18 02:03:51 +00:00
|
|
|
var view = self.getView(viewId);
|
2015-04-27 02:46:16 +00:00
|
|
|
|
|
|
|
if(!view) {
|
2015-07-04 22:03:44 +00:00
|
|
|
self.client.log.warn( { viewId : viewId }, 'Cannot find view');
|
2015-04-27 02:46:16 +00:00
|
|
|
nextItem(null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-09-03 05:11:17 +00:00
|
|
|
var mciConf = config.mci[mci];
|
2015-04-20 04:58:18 +00:00
|
|
|
|
|
|
|
self.setViewPropertiesFromMCIConf(view, mciConf);
|
|
|
|
|
|
|
|
if(mciConf.focus) {
|
|
|
|
initialFocusId = viewId;
|
|
|
|
}
|
|
|
|
|
|
|
|
nextItem(null);
|
|
|
|
},
|
2015-07-10 17:11:08 +00:00
|
|
|
function complete(err) {
|
2015-04-20 04:58:18 +00:00
|
|
|
// default to highest ID if no 'submit' entry present
|
|
|
|
if(!submitId) {
|
2015-09-23 05:13:06 +00:00
|
|
|
var highestIdView = self.getView(highestId);
|
|
|
|
if(highestIdView) {
|
|
|
|
highestIdView.submit = true;
|
|
|
|
} else {
|
|
|
|
self.client.log.warn( { highestId : highestId }, 'View does not exist');
|
|
|
|
}
|
2015-04-20 04:58:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cb(err, { initialFocusId : initialFocusId } );
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-08-07 05:08:10 +00:00
|
|
|
// method for comparing submitted form data to configuration entries
|
|
|
|
this.actionBlockValueComparator = function(formValue, actionValue) {
|
|
|
|
//
|
|
|
|
// For a match to occur, one of the following must be true:
|
|
|
|
//
|
|
|
|
// * actionValue is a Object:
|
|
|
|
// a) All key/values must exactly match
|
|
|
|
// b) value is null; The key (view ID or "argName") must be present
|
|
|
|
// in formValue. This is a wildcard/any match.
|
|
|
|
// * actionValue is a Number: This represents a view ID that
|
|
|
|
// must be present in formValue.
|
|
|
|
// * actionValue is a string: This represents a view with
|
|
|
|
// "argName" set that must be present in formValue.
|
|
|
|
//
|
2015-08-19 04:45:47 +00:00
|
|
|
if(_.isUndefined(actionValue)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-08-07 05:08:10 +00:00
|
|
|
if(_.isNumber(actionValue) || _.isString(actionValue)) {
|
|
|
|
if(_.isUndefined(formValue[actionValue])) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else {
|
2016-07-25 20:35:58 +00:00
|
|
|
/*
|
|
|
|
:TODO: support:
|
|
|
|
value: {
|
|
|
|
someArgName: [ "key1", "key2", ... ],
|
|
|
|
someOtherArg: [ "key1, ... ]
|
|
|
|
}
|
|
|
|
*/
|
2015-08-07 05:08:10 +00:00
|
|
|
var actionValueKeys = Object.keys(actionValue);
|
|
|
|
for(var i = 0; i < actionValueKeys.length; ++i) {
|
|
|
|
var viewId = actionValueKeys[i];
|
|
|
|
if(!_.has(formValue, viewId)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(null !== actionValue[viewId] && actionValue[viewId] !== formValue[viewId]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-05 03:36:26 +00:00
|
|
|
self.client.log.trace(
|
|
|
|
{
|
|
|
|
formValue : formValue,
|
|
|
|
actionValue : actionValue
|
|
|
|
},
|
|
|
|
'Action match'
|
|
|
|
);
|
|
|
|
|
2015-08-07 05:08:10 +00:00
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2015-07-04 18:02:37 +00:00
|
|
|
if(!options.detached) {
|
|
|
|
this.attachClientEvents();
|
|
|
|
}
|
2015-09-20 04:55:09 +00:00
|
|
|
|
|
|
|
this.setViewFocusWithEvents = function(view, focused) {
|
|
|
|
if(!view || !view.acceptsFocus) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(focused) {
|
|
|
|
self.switchFocusEvent('return', view);
|
|
|
|
self.focusedView = view;
|
|
|
|
} else {
|
|
|
|
self.switchFocusEvent('leave', view);
|
|
|
|
}
|
|
|
|
|
|
|
|
view.setFocus(focused);
|
|
|
|
};
|
2015-12-12 22:52:56 +00:00
|
|
|
|
|
|
|
this.validateView = function(view, cb) {
|
|
|
|
if(view && _.isFunction(view.validate)) {
|
|
|
|
view.validate(view.getData(), function validateResult(err) {
|
|
|
|
var viewValidationListener = self.client.currentMenuModule.menuMethods.viewValidationListener;
|
|
|
|
if(_.isFunction(viewValidationListener)) {
|
|
|
|
if(err) {
|
|
|
|
err.view = view; // pass along the view that failed
|
|
|
|
}
|
|
|
|
|
|
|
|
viewValidationListener(err, function validationComplete(newViewFocusId) {
|
|
|
|
cb(err, newViewFocusId);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
cb(err);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
cb(null);
|
|
|
|
}
|
|
|
|
};
|
2014-10-23 05:41:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
util.inherits(ViewController, events.EventEmitter);
|
|
|
|
|
|
|
|
ViewController.prototype.attachClientEvents = function() {
|
|
|
|
if(this.attached) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-07-12 02:12:07 +00:00
|
|
|
var self = this;
|
|
|
|
|
2015-04-21 04:50:58 +00:00
|
|
|
this.client.on('key press', this.clientKeyPressHandler);
|
2014-10-23 05:41:00 +00:00
|
|
|
|
2015-07-12 02:12:07 +00:00
|
|
|
Object.keys(this.views).forEach(function vid(i) {
|
|
|
|
// remove, then add to ensure we only have one listener
|
|
|
|
self.views[i].removeListener('action', self.viewActionListener);
|
|
|
|
self.views[i].on('action', self.viewActionListener);
|
|
|
|
});
|
|
|
|
|
2014-10-23 05:41:00 +00:00
|
|
|
this.attached = true;
|
|
|
|
};
|
|
|
|
|
|
|
|
ViewController.prototype.detachClientEvents = function() {
|
|
|
|
if(!this.attached) {
|
|
|
|
return;
|
|
|
|
}
|
2015-04-19 08:13:13 +00:00
|
|
|
|
2015-04-21 04:50:58 +00:00
|
|
|
this.client.removeListener('key press', this.clientKeyPressHandler);
|
2014-10-23 05:41:00 +00:00
|
|
|
|
2014-11-04 07:34:54 +00:00
|
|
|
for(var id in this.views) {
|
|
|
|
this.views[id].removeAllListeners();
|
|
|
|
}
|
|
|
|
|
2014-10-23 05:41:00 +00:00
|
|
|
this.attached = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
ViewController.prototype.viewExists = function(id) {
|
|
|
|
return id in this.views;
|
|
|
|
};
|
|
|
|
|
|
|
|
ViewController.prototype.addView = function(view) {
|
|
|
|
assert(!this.viewExists(view.id), 'View with ID ' + view.id + ' already exists');
|
|
|
|
|
|
|
|
this.views[view.id] = view;
|
|
|
|
};
|
|
|
|
|
|
|
|
ViewController.prototype.getView = function(id) {
|
|
|
|
return this.views[id];
|
|
|
|
};
|
|
|
|
|
2015-04-12 05:48:41 +00:00
|
|
|
ViewController.prototype.getFocusedView = function() {
|
|
|
|
return this.focusedView;
|
|
|
|
};
|
|
|
|
|
2015-07-09 04:34:40 +00:00
|
|
|
ViewController.prototype.setFocus = function(focused) {
|
|
|
|
if(focused) {
|
|
|
|
this.attachClientEvents();
|
|
|
|
} else {
|
|
|
|
this.detachClientEvents();
|
2015-07-04 18:02:37 +00:00
|
|
|
}
|
2015-09-20 04:55:09 +00:00
|
|
|
|
2015-09-21 01:10:09 +00:00
|
|
|
this.setViewFocusWithEvents(this.focusedView, focused);
|
2015-07-04 18:02:37 +00:00
|
|
|
};
|
|
|
|
|
2016-10-25 03:49:45 +00:00
|
|
|
ViewController.prototype.resetInitialFocus = function() {
|
|
|
|
if(this.formInitialFocusId) {
|
|
|
|
return this.switchFocus(this.formInitialFocusId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-23 05:41:00 +00:00
|
|
|
ViewController.prototype.switchFocus = function(id) {
|
2015-12-10 07:04:38 +00:00
|
|
|
//
|
|
|
|
// Perform focus switching validation now
|
|
|
|
//
|
|
|
|
var self = this;
|
|
|
|
var focusedView = self.focusedView;
|
|
|
|
|
2015-12-12 22:52:56 +00:00
|
|
|
self.validateView(focusedView, function validated(err, newFocusedViewId) {
|
|
|
|
if(err) {
|
|
|
|
var newFocusedView = self.getView(newFocusedViewId) || focusedView;
|
|
|
|
self.setViewFocusWithEvents(newFocusedView, true);
|
|
|
|
} else {
|
|
|
|
self.attachClientEvents();
|
2015-07-09 04:34:40 +00:00
|
|
|
|
2015-12-12 22:52:56 +00:00
|
|
|
// remove from old
|
|
|
|
self.setViewFocusWithEvents(focusedView, false);
|
2015-04-14 06:19:14 +00:00
|
|
|
|
2015-12-12 22:52:56 +00:00
|
|
|
// set to new
|
|
|
|
self.setViewFocusWithEvents(self.getView(id), true);
|
|
|
|
}
|
|
|
|
});
|
2014-10-23 05:41:00 +00:00
|
|
|
};
|
|
|
|
|
2015-12-10 07:04:38 +00:00
|
|
|
ViewController.prototype.nextFocus = function() {
|
|
|
|
var nextId;
|
|
|
|
|
2014-10-23 05:41:00 +00:00
|
|
|
if(!this.focusedView) {
|
2015-12-10 07:04:38 +00:00
|
|
|
nextId = this.views[this.firstId].id;
|
2014-10-23 05:41:00 +00:00
|
|
|
} else {
|
2015-12-10 07:04:38 +00:00
|
|
|
nextId = this.views[this.focusedView.id].nextId;
|
2014-10-23 05:41:00 +00:00
|
|
|
}
|
2015-12-10 07:04:38 +00:00
|
|
|
|
|
|
|
this.switchFocus(nextId);
|
2014-10-23 05:41:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
ViewController.prototype.setViewOrder = function(order) {
|
|
|
|
var viewIdOrder = order || [];
|
|
|
|
|
|
|
|
if(0 === viewIdOrder.length) {
|
|
|
|
for(var id in this.views) {
|
2014-10-28 03:58:34 +00:00
|
|
|
if(this.views[id].acceptsFocus) {
|
|
|
|
viewIdOrder.push(id);
|
|
|
|
}
|
2014-10-23 05:41:00 +00:00
|
|
|
}
|
|
|
|
|
2014-10-28 03:58:34 +00:00
|
|
|
viewIdOrder.sort(function intSort(a, b) {
|
|
|
|
return a - b;
|
|
|
|
});
|
2014-10-23 05:41:00 +00:00
|
|
|
}
|
|
|
|
|
2014-10-31 22:25:11 +00:00
|
|
|
if(viewIdOrder.length > 0) {
|
|
|
|
var view;
|
|
|
|
var count = viewIdOrder.length - 1;
|
|
|
|
for(var i = 0; i < count; ++i) {
|
|
|
|
this.views[viewIdOrder[i]].nextId = viewIdOrder[i + 1];
|
|
|
|
}
|
2014-10-23 05:41:00 +00:00
|
|
|
|
2014-10-31 22:25:11 +00:00
|
|
|
this.firstId = viewIdOrder[0];
|
|
|
|
var lastId = viewIdOrder.length > 1 ? viewIdOrder[viewIdOrder.length - 1] : this.firstId;
|
|
|
|
this.views[lastId].nextId = this.firstId;
|
|
|
|
}
|
2014-10-23 05:41:00 +00:00
|
|
|
};
|
|
|
|
|
2015-06-26 04:34:33 +00:00
|
|
|
ViewController.prototype.redrawAll = function(initialFocusId) {
|
2015-07-06 01:05:55 +00:00
|
|
|
this.client.term.rawWrite(ansi.hideCursor());
|
2015-06-26 04:34:33 +00:00
|
|
|
|
|
|
|
for(var id in this.views) {
|
|
|
|
if(initialFocusId === id) {
|
|
|
|
continue; // will draw @ focus
|
|
|
|
}
|
|
|
|
this.views[id].redraw();
|
|
|
|
}
|
|
|
|
|
2015-07-06 01:05:55 +00:00
|
|
|
this.client.term.rawWrite(ansi.showCursor());
|
2015-06-26 04:34:33 +00:00
|
|
|
};
|
|
|
|
|
2015-04-20 04:58:18 +00:00
|
|
|
ViewController.prototype.loadFromPromptConfig = function(options, cb) {
|
2015-04-19 08:13:13 +00:00
|
|
|
assert(_.isObject(options));
|
|
|
|
assert(_.isObject(options.mciMap));
|
2015-04-21 04:50:58 +00:00
|
|
|
|
2015-04-19 08:13:13 +00:00
|
|
|
var self = this;
|
2015-07-25 22:10:12 +00:00
|
|
|
var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig;
|
2015-04-19 08:13:13 +00:00
|
|
|
var initialFocusId = 1; // default to first
|
|
|
|
|
|
|
|
async.waterfall(
|
|
|
|
[
|
|
|
|
function createViewsFromMCI(callback) {
|
2015-04-20 04:58:18 +00:00
|
|
|
self.createViewsFromMCI(options.mciMap, function viewsCreated(err) {
|
2015-04-19 08:13:13 +00:00
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
},
|
2015-04-20 04:58:18 +00:00
|
|
|
function applyViewConfiguration(callback) {
|
2015-05-01 04:29:24 +00:00
|
|
|
if(_.isObject(promptConfig.mci)) {
|
|
|
|
self.applyViewConfig(promptConfig, function configApplied(err, info) {
|
|
|
|
initialFocusId = info.initialFocusId;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
callback(null);
|
|
|
|
}
|
2015-04-19 08:13:13 +00:00
|
|
|
},
|
2015-08-07 05:08:10 +00:00
|
|
|
function prepareFormSubmission(callback) {
|
2015-07-25 22:10:12 +00:00
|
|
|
if(false === self.noInput) {
|
2016-07-25 20:35:58 +00:00
|
|
|
|
2015-07-25 22:10:12 +00:00
|
|
|
self.on('submit', function promptSubmit(formData) {
|
|
|
|
self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Prompt submit');
|
2015-04-19 08:13:13 +00:00
|
|
|
|
2015-08-07 05:08:10 +00:00
|
|
|
if(_.isString(self.client.currentMenuModule.menuConfig.action)) {
|
2016-07-25 20:35:58 +00:00
|
|
|
self.handleActionWrapper(formData, self.client.currentMenuModule.menuConfig);
|
2015-08-07 05:08:10 +00:00
|
|
|
} else {
|
|
|
|
//
|
|
|
|
// Menus that reference prompts can have a sepcial "submit" block without the
|
|
|
|
// hassle of by-form-id configurations, etc.
|
|
|
|
//
|
|
|
|
// "submit" : [
|
|
|
|
// { ... }
|
|
|
|
// ]
|
|
|
|
//
|
|
|
|
var menuSubmit = self.client.currentMenuModule.menuConfig.submit;
|
|
|
|
if(!_.isArray(menuSubmit)) {
|
|
|
|
self.client.log.debug('No configuration to handle submit');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Locate matching action block
|
|
|
|
//
|
2016-07-25 06:11:04 +00:00
|
|
|
// :TODO: this is basically the same as for menus -- DRY it up!
|
2015-08-07 05:08:10 +00:00
|
|
|
for(var c = 0; c < menuSubmit.length; ++c) {
|
|
|
|
var actionBlock = menuSubmit[c];
|
|
|
|
|
|
|
|
if(_.isEqual(formData.value, actionBlock.value, self.actionBlockValueComparator)) {
|
2016-07-25 20:35:58 +00:00
|
|
|
self.handleActionWrapper(formData, actionBlock);
|
2015-08-07 05:08:10 +00:00
|
|
|
break; // there an only be one...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-07-25 22:10:12 +00:00
|
|
|
});
|
|
|
|
}
|
2015-04-19 08:13:13 +00:00
|
|
|
|
|
|
|
callback(null);
|
|
|
|
},
|
2015-04-27 02:46:16 +00:00
|
|
|
function drawAllViews(callback) {
|
2015-06-26 04:34:33 +00:00
|
|
|
self.redrawAll(initialFocusId);
|
2015-04-27 02:46:16 +00:00
|
|
|
callback(null);
|
|
|
|
},
|
2015-04-20 04:58:18 +00:00
|
|
|
function setInitialViewFocus(callback) {
|
2015-04-19 08:13:13 +00:00
|
|
|
if(initialFocusId) {
|
|
|
|
self.switchFocus(initialFocusId);
|
|
|
|
}
|
2015-04-20 04:58:18 +00:00
|
|
|
callback(null);
|
2015-04-19 08:13:13 +00:00
|
|
|
}
|
|
|
|
],
|
|
|
|
function complete(err) {
|
|
|
|
cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2015-04-20 04:58:18 +00:00
|
|
|
ViewController.prototype.loadFromMenuConfig = function(options, cb) {
|
2015-04-19 08:13:13 +00:00
|
|
|
assert(_.isObject(options));
|
2015-08-27 05:04:04 +00:00
|
|
|
|
|
|
|
if(!_.isObject(options.mciMap)) {
|
|
|
|
cb(new Error('Missing option: mciMap'));
|
|
|
|
return;
|
|
|
|
}
|
2015-04-20 04:58:18 +00:00
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var formIdKey = options.formId ? options.formId.toString() : '0';
|
2016-10-25 03:49:45 +00:00
|
|
|
this.formInitialFocusId = 1; // default to first
|
2015-04-20 04:58:18 +00:00
|
|
|
var formConfig;
|
|
|
|
|
|
|
|
// :TODO: honor options.withoutForm
|
|
|
|
|
2015-04-19 08:13:13 +00:00
|
|
|
async.waterfall(
|
|
|
|
[
|
2015-04-20 04:58:18 +00:00
|
|
|
function findMatchingFormConfig(callback) {
|
2015-04-21 04:50:58 +00:00
|
|
|
menuUtil.getFormConfigByIDAndMap(self.client.currentMenuModule.menuConfig, formIdKey, options.mciMap, function matchingConfig(err, fc) {
|
2015-04-20 04:58:18 +00:00
|
|
|
formConfig = fc;
|
2015-04-19 08:13:13 +00:00
|
|
|
|
2015-04-20 04:58:18 +00:00
|
|
|
if(err) {
|
|
|
|
// non-fatal
|
2015-07-04 22:03:44 +00:00
|
|
|
self.client.log.trace(
|
2016-07-20 03:00:56 +00:00
|
|
|
{ reason : err.message, mci : Object.keys(options.mciMap), formId : formIdKey },
|
2015-04-20 04:58:18 +00:00
|
|
|
'Unable to find matching form configuration');
|
2015-04-19 08:13:13 +00:00
|
|
|
}
|
|
|
|
|
2015-04-20 04:58:18 +00:00
|
|
|
callback(null);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function createViews(callback) {
|
|
|
|
self.createViewsFromMCI(options.mciMap, function viewsCreated(err) {
|
2015-04-19 08:13:13 +00:00
|
|
|
callback(err);
|
|
|
|
});
|
2015-04-20 04:58:18 +00:00
|
|
|
},
|
2016-01-15 05:58:56 +00:00
|
|
|
/*
|
2015-05-14 04:21:55 +00:00
|
|
|
function applyThemeCustomization(callback) {
|
2015-10-10 05:35:40 +00:00
|
|
|
formConfig = formConfig || {};
|
2015-10-18 02:03:51 +00:00
|
|
|
formConfig.mci = formConfig.mci || {};
|
2015-10-10 05:35:40 +00:00
|
|
|
//self.client.currentMenuModule.menuConfig.config = self.client.currentMenuModule.menuConfig.config || {};
|
|
|
|
|
|
|
|
//console.log('menu config.....');
|
|
|
|
//console.log(self.client.currentMenuModule.menuConfig)
|
|
|
|
|
|
|
|
menuUtil.applyMciThemeCustomization({
|
|
|
|
name : self.client.currentMenuModule.menuName,
|
|
|
|
type : 'menus',
|
|
|
|
client : self.client,
|
|
|
|
mci : formConfig.mci,
|
|
|
|
//config : self.client.currentMenuModule.menuConfig.config,
|
|
|
|
formId : formIdKey,
|
|
|
|
});
|
2015-09-28 04:05:40 +00:00
|
|
|
|
2015-10-10 05:35:40 +00:00
|
|
|
//console.log('after theme...')
|
|
|
|
//console.log(self.client.currentMenuModule.menuConfig.config)
|
|
|
|
|
2015-05-14 04:21:55 +00:00
|
|
|
callback(null);
|
|
|
|
},
|
2016-01-15 05:58:56 +00:00
|
|
|
*/
|
2015-04-20 04:58:18 +00:00
|
|
|
function applyViewConfiguration(callback) {
|
|
|
|
if(_.isObject(formConfig)) {
|
|
|
|
self.applyViewConfig(formConfig, function configApplied(err, info) {
|
2016-10-25 03:49:45 +00:00
|
|
|
self.formInitialFocusId = info.initialFocusId;
|
2015-04-20 04:58:18 +00:00
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
callback(null);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
function prepareFormSubmission(callback) {
|
|
|
|
if(!_.isObject(formConfig) || !_.isObject(formConfig.submit)) {
|
|
|
|
callback(null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.on('submit', function formSubmit(formData) {
|
2015-06-27 21:32:29 +00:00
|
|
|
|
2015-07-04 22:03:44 +00:00
|
|
|
self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Form submit');
|
2015-04-20 04:58:18 +00:00
|
|
|
|
|
|
|
//
|
|
|
|
// Locate configuration for this form ID
|
|
|
|
//
|
|
|
|
var confForFormId;
|
|
|
|
if(_.isObject(formConfig.submit[formData.submitId])) {
|
|
|
|
confForFormId = formConfig.submit[formData.submitId];
|
|
|
|
} else if(_.isObject(formConfig.submit['*'])) {
|
|
|
|
confForFormId = formConfig.submit['*'];
|
|
|
|
} else {
|
|
|
|
// no configuration for this submitId
|
2015-07-09 04:34:40 +00:00
|
|
|
self.client.log.debug( { formId : formData.submitId }, 'No configuration for form ID');
|
2015-04-20 04:58:18 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Locate a matching action block based on the submitted data
|
|
|
|
//
|
|
|
|
for(var c = 0; c < confForFormId.length; ++c) {
|
|
|
|
var actionBlock = confForFormId[c];
|
|
|
|
|
2015-08-07 05:08:10 +00:00
|
|
|
if(_.isEqual(formData.value, actionBlock.value, self.actionBlockValueComparator)) {
|
2016-07-25 20:35:58 +00:00
|
|
|
self.handleActionWrapper(formData, actionBlock);
|
2015-04-20 04:58:18 +00:00
|
|
|
break; // there an only be one...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
},
|
2015-07-10 05:23:37 +00:00
|
|
|
function loadActionKeys(callback) {
|
|
|
|
if(!_.isObject(formConfig) || !_.isArray(formConfig.actionKeys)) {
|
|
|
|
callback(null);
|
|
|
|
return;
|
|
|
|
}
|
2015-09-20 04:55:09 +00:00
|
|
|
|
2015-07-10 05:23:37 +00:00
|
|
|
formConfig.actionKeys.forEach(function akEntry(ak) {
|
|
|
|
//
|
|
|
|
// * 'keys' must be present and be an array of key names
|
|
|
|
// * If 'viewId' is present, key(s) will focus & submit on behalf
|
|
|
|
// of the specified view.
|
|
|
|
// * If 'action' is present, that action will be procesed when
|
|
|
|
// triggered by key(s)
|
|
|
|
//
|
2015-09-19 04:16:19 +00:00
|
|
|
// Ultimately, create a map of key -> { action block }
|
2015-07-10 05:23:37 +00:00
|
|
|
//
|
|
|
|
if(!_.isArray(ak.keys)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
ak.keys.forEach(function actionKeyName(kn) {
|
2015-09-19 04:16:19 +00:00
|
|
|
self.actionKeyMap[kn] = ak;
|
2015-07-10 05:23:37 +00:00
|
|
|
});
|
2015-09-19 04:16:19 +00:00
|
|
|
|
2015-07-10 05:23:37 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
},
|
2015-04-27 02:46:16 +00:00
|
|
|
function drawAllViews(callback) {
|
2016-10-25 03:49:45 +00:00
|
|
|
self.redrawAll(self.formInitialFocusId);
|
2015-04-27 02:46:16 +00:00
|
|
|
callback(null);
|
|
|
|
},
|
2015-04-20 04:58:18 +00:00
|
|
|
function setInitialViewFocus(callback) {
|
2016-10-25 03:49:45 +00:00
|
|
|
if(self.formInitialFocusId) {
|
|
|
|
self.switchFocus(self.formInitialFocusId);
|
2015-04-20 04:58:18 +00:00
|
|
|
}
|
|
|
|
callback(null);
|
2015-04-19 08:13:13 +00:00
|
|
|
}
|
|
|
|
],
|
2015-04-20 04:58:18 +00:00
|
|
|
function complete(err) {
|
|
|
|
if(_.isFunction(cb)) {
|
|
|
|
cb(err);
|
|
|
|
}
|
2015-04-19 08:13:13 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2015-04-04 20:41:04 +00:00
|
|
|
ViewController.prototype.formatMCIString = function(format) {
|
|
|
|
var self = this;
|
|
|
|
var view;
|
|
|
|
|
|
|
|
return format.replace(/{(\d+)}/g, function replacer(match, number) {
|
|
|
|
view = self.getView(number);
|
|
|
|
|
|
|
|
if(!view) {
|
|
|
|
return match;
|
|
|
|
}
|
|
|
|
|
2015-04-17 04:29:53 +00:00
|
|
|
return view.getData();
|
2015-04-04 20:41:04 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-08-13 22:05:17 +00:00
|
|
|
ViewController.prototype.getFormData = function(key) {
|
2015-07-04 18:02:37 +00:00
|
|
|
/*
|
|
|
|
Example form data:
|
|
|
|
{
|
|
|
|
id : 0,
|
|
|
|
submitId : 1,
|
|
|
|
value : {
|
|
|
|
"1" : "hurp",
|
|
|
|
"2" : [ 'a', 'b', ... ],
|
|
|
|
"3" 2,
|
|
|
|
"pants" : "no way"
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
*/
|
2016-10-25 03:49:45 +00:00
|
|
|
const formData = {
|
2015-07-04 18:02:37 +00:00
|
|
|
id : this.formId,
|
|
|
|
submitId : this.focusedView.id,
|
|
|
|
value : {},
|
|
|
|
};
|
|
|
|
|
2015-08-14 04:30:55 +00:00
|
|
|
if(key) {
|
2015-08-13 22:05:17 +00:00
|
|
|
formData.key = key;
|
|
|
|
}
|
|
|
|
|
2016-10-25 03:49:45 +00:00
|
|
|
let viewData;
|
|
|
|
_.each(this.views, view => {
|
|
|
|
try {
|
|
|
|
// don't fill forms with static, non user-editable data data
|
|
|
|
if(!view.acceptsInput) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
viewData = view.getData();
|
|
|
|
if(_.isUndefined(viewData)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
formData.value[ view.submitArgName ? view.submitArgName : view.id ] = viewData;
|
|
|
|
} catch(e) {
|
|
|
|
this.client.log.error( { error : e.message }, 'Exception caught gathering form data' );
|
|
|
|
}
|
|
|
|
});
|
|
|
|
/*
|
|
|
|
|
2015-07-04 18:02:37 +00:00
|
|
|
var viewData;
|
|
|
|
var view;
|
|
|
|
for(var id in this.views) {
|
|
|
|
try {
|
|
|
|
view = this.views[id];
|
|
|
|
viewData = view.getData();
|
|
|
|
if(!_.isUndefined(viewData)) {
|
|
|
|
if(_.isString(view.submitArgName)) {
|
|
|
|
formData.value[view.submitArgName] = viewData;
|
|
|
|
} else {
|
|
|
|
formData.value[id] = viewData;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch(e) {
|
2015-07-22 05:52:20 +00:00
|
|
|
this.client.log.error(e); // :TODO: Log better ;)
|
2015-07-04 18:02:37 +00:00
|
|
|
}
|
2016-10-25 03:49:45 +00:00
|
|
|
}*/
|
2015-07-04 18:02:37 +00:00
|
|
|
|
|
|
|
return formData;
|
2016-10-25 03:49:45 +00:00
|
|
|
};
|
2015-07-04 18:02:37 +00:00
|
|
|
|
2015-04-21 04:50:58 +00:00
|
|
|
/*
|
2015-04-04 20:41:04 +00:00
|
|
|
ViewController.prototype.formatMenuArgs = function(args) {
|
|
|
|
var self = this;
|
|
|
|
|
2015-04-05 07:15:04 +00:00
|
|
|
return _.mapValues(args, function val(value) {
|
2015-04-04 20:41:04 +00:00
|
|
|
if('string' === typeof value) {
|
2015-04-05 07:15:04 +00:00
|
|
|
return self.formatMCIString(value);
|
2015-04-04 20:41:04 +00:00
|
|
|
}
|
2015-04-05 07:15:04 +00:00
|
|
|
return value;
|
2015-04-04 20:41:04 +00:00
|
|
|
});
|
2015-04-21 04:50:58 +00:00
|
|
|
};
|
|
|
|
*/
|