import { observable, computed } from 'knockout';
import { evaluate } from 'scalejs.expression-jsep';
import { has, get, is } from 'scalejs';
import _ from 'lodash';
/**
* select is a type of input that lets the
* user select the value from dropdown options
*
* @module input-select
*
* @param {object} node
* The configuration specs for the component.
* @param {string} node.options
* Options specific to this component
* @param {bool} [node.options.addBlank=true]
* Whether or not to add a blank to the beginning of the select options
* @param {object|array} node.options.values
* Specifies either an array of values to use or an object describing the values
* @param {string} node.options.values.fromArray
* A string to be evaluated that will be used to map the values
* @param {string|array} node.options.values.textKey
* A string or array which refers to the text key (i.e. the label) for the options
* @param {string} node.options.values.valueKey
* A string which referes to the value key for the options
* @param {string} [node.options.values.delimeter=' / ']
* A delimeter for the label if the textKey is an array
* @param {string} [node.options.values.textFormatter]
* The name of a function to format the label for the option (i.e. dateFormatter)
*
*/
export default function selectViewModel(node, inputViewModel) {
const context = this,
options = node.options || { values: [] },
// inputViewModel
inputValue = inputViewModel.inputValue,
mapItem = inputViewModel.mapItem,
format = inputViewModel.format,
subs = inputViewModel.subs,
values = inputViewModel.values,
// props
addBlank = !has(options.addBlank) || options.addBlank,
currentFilter = observable();
let computedValues = {};
if (!options.values) {
console.warn('select input type being used without values');
options.values = [];
}
/**
* Helper function to check if the array has the value
*
* @param {array} valuesArr Array to check for value
* @param value The value to check in array
*/
function arrayHasValue(valuesArr, valueOrObjectToCheck) {
const valueToCheck = get(valueOrObjectToCheck, 'value', valueOrObjectToCheck);
return valuesArr.some(value => get(value, 'value', value) === valueToCheck);
}
/**
* Helper function to takes valuesArr and a value.
* If the array does not contain the value, it unshifts it
*
* @param {array} valuesArr Array to check for value
* @param value The value to unshift if not found
*/
function unshiftToValues(valuesArr, value) {
const hasValue = arrayHasValue(valuesArr, value);
if (options.unshiftToValues && !hasValue && has(value) && value !== '') {
valuesArr.unshift({
text: format(value),
value
});
}
return valuesArr;
}
/**
* Sets the values if the options.values is an array.
* Maps any string values to { text, value }
*/
function setValuesFromOptionsArray() {
values((addBlank ? [''] : []).concat(options.values.slice()).map(val => (
is(val, 'string') ? { text: val, value: val } : val
)));
}
/**
* Sets the values if the options.values is an object and options.values.fromArray exists.
* fromArray is an expression which is evaluated to retrieve the values from context
*/
function setValuesFromOptionsObject() {
// create a sub to subscribe to changes in values
subs.push(computed(() => {
const value = inputValue.peek();
let newValues = (_.toArray(evaluate(options.values.fromArray, context.getValue) || []))
.filter(item => has(item))
.map(mapItem(options.values));
newValues = (addBlank || newValues.length === 0 ? [{ text: '', value: '' }] : []).concat(newValues);
values(unshiftToValues(newValues, value));
}).extend({ deferred: true }));
}
/**
* setValue is Utilized by the form to set the value of the input after initialization
* If the value is not already in the values array, it will be unshifted
*
* @param {object|value} data Either an object with a value or the value to be set
*/
function setValue(data) {
const value = is(data, 'object') &&
{}.hasOwnProperty.call(data, 'value') ? data.value : data;
values(unshiftToValues(values(), value));
inputValue(value);
}
/**
* Function which is utilized by rules engine
* Sets the currentFilter observable
* This will make the computedValues return only the valuesToKeep
*
* @param {array} valuesToKeep Array of values that will kept in the filter
*/
function filterValues(valuesToKeep) {
currentFilter(valuesToKeep);
// changing the currentFilter can change the values
// this in turn, changing the inputValue. But for some reason,
// its not enough to trigger bindings.
// manually provoke a change to isModified, so validation bindings get re-evaled.
inputValue.isModified.valueHasMutated();
}
/**
* Initialize the values observable either with array or with object
*/
if (Array.isArray(options.values)) { setValuesFromOptionsArray(); }
if (options.values.fromArray) { setValuesFromOptionsObject(); }
/**
* If currentFilter is defined, return only values which match
*/
computedValues = computed({
read: function () {
if (!currentFilter()) {
return values();
}
return values().filter((v) => {
// || v; //we used to expect { value: ''} or '', now we always do mapping first
const value = v.value;
return currentFilter().indexOf(value) !== -1;
});
},
write: function (newValues) {
values(newValues);
}
});
return {
values: computedValues,
setValue,
filterValues
};
}