MediaWiki:Gadget-calculator-core.js
From WikiAnesthesia
Revision as of 10:11, 20 July 2021 by Chris Rishel (talk | contribs)
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/**
* @author Chris Rishel
*/
( function() {
const COOKIE_EXPIRATION = 12 * 60 * 60;
const RENDERFORMAT_PLAIN = 'plain';
const RENDERFORMAT_ROW = 'row';
const VALID_RENDERFORMATS = [
RENDERFORMAT_PLAIN,
RENDERFORMAT_ROW
];
const RENDERLABEL_NAME = 'name';
const RENDERLABEL_ABBREVIATION = 'abbreviation';
const VALID_RENDERLABELS = [
RENDERLABEL_NAME,
RENDERLABEL_ABBREVIATION
];
const INPUTSIZE_COMPACT = 'compact';
const INPUTSIZE_FULL = 'full';
const VALID_INPUTSIZES = [
INPUTSIZE_COMPACT,
INPUTSIZE_FULL
];
const TYPE_NUMBER = 'number';
const TYPE_STRING = 'string';
const VALID_TYPES = [
TYPE_NUMBER,
TYPE_STRING
];
/**
* Class CalculatorObject
*
* @param {Object} varData
* @param {Object} propertyData
* @returns {Object}
* @constructor
*/
function CalculatorObject( varData, propertyData ) {
if( propertyData ) {
if( propertyData.hasOwnProperty( 'required' ) ) {
for( var iRequiredProperty in propertyData.required ) {
var requiredProperty = propertyData.required[ iRequiredProperty ];
if( !varData.hasOwnProperty( requiredProperty ) ) {
console.error( 'Missing required property "' + requiredProperty + '"' );
console.log( varData );
return null;
}
this[ requiredProperty ] = varData[ requiredProperty ];
}
}
if( propertyData.hasOwnProperty( 'optional' ) ) {
for( var iOptionalProperty in propertyData.optional ) {
var optionalProperty = propertyData.optional[ iOptionalProperty ];
if( varData.hasOwnProperty( optionalProperty ) ) {
this[ optionalProperty ] = varData[ optionalProperty ];
} else {
this[ optionalProperty ] = null;
}
}
}
}
}
/**
* Class CalculatorVariable
* @param {Object} varData
* @returns {CalculatorVariable}
* @constructor
*/
function CalculatorVariable( varData ) {
var propertyData = {
required: [
'id',
'name',
'type'
],
optional: [
'abbreviation',
'defaultValue',
'maxLength',
'options',
'renderUnits',
'units'
]
};
CalculatorObject.call( this, varData, propertyData );
if( VALID_TYPES.indexOf( this.type ) === -1 ) {
throw new Error( 'Invalid type "' + this.type + '" for variable "' + this.id + '"' );
}
this.calculations = [];
if( this.defaultValue ) {
this.setValue( this.defaultValue );
} else {
this.value = null;
}
}
CalculatorVariable.prototype = Object.create( CalculatorObject.prototype );
CalculatorVariable.prototype.addCalculation = function( calcId ) {
if( this.calculations.indexOf( calcId ) !== -1 ) {
return;
}
this.calculations.push( calcId );
};
CalculatorVariable.prototype.createInput = function( options ) {
// Initialize options
options = ( typeof options !== 'undefined' ) ? options : {};
if( !options.hasOwnProperty( 'size' ) ) {
options.size = 'full';
} else if( VALID_INPUTSIZES.indexOf( options.size ) === -1 ) {
throw new Error( 'Invalid input size "' + options.size + '" for variable "' + this.getId() + '"' );
}
var varId = this.getId();
var value = this.getValue();
var inputContainerAttribs = {
class: 'col-auto calculator-container-input'
};
if( options.size === INPUTSIZE_COMPACT ) {
inputContainerAttribs.class = inputContainerAttribs.class + ' calculator-container-input-compact';
}
inputContainerAttribs.class = inputContainerAttribs.class + ' calculator-container-input-' + this.getName();
// Create the input container
var $inputContainer = $( '<div>', inputContainerAttribs );
// Set the input id
var inputId = 'calculator-input-' + varId;
if( this.getType() === TYPE_NUMBER ) {
// Initialize the primary units variables (needed for handlers, even if doesn't have units)
var unitsId = null;
var $unitsContainer = null;
// Initialize label attributes
var labelAttributes = {
for: inputId,
text: this.getName()
};
// Initialize input options
var inputAttributes = {
id: inputId,
class: 'form-control calculator-input-text',
type: 'text',
inputmode: 'decimal',
placeholder: this.getName(),
value: this.isValueMathObject() ? value.toNumber() : value
};
// Configure additional options
if( this.maxLength ) {
inputAttributes.maxlength = this.maxLength;
}
if( options.size === INPUTSIZE_COMPACT ) {
// Only use label for screen readers
labelAttributes.class = 'sr-only';
inputAttributes.class = inputAttributes.class + ' calculator-input-text-compact';
// Switch the placeholder to the abbreviation if defined
if( this.abbreviation ) {
inputAttributes.placeholder = this.abbreviation;
}
// Set the size of the input to the maxlength if defined
if( inputAttributes.hasOwnProperty( 'maxlength' ) ) {
inputAttributes.size = inputAttributes.maxlength;
}
}
// Add the input id to the list of classes
inputAttributes.class = inputAttributes.class + ' ' + inputId;
// If the variable has units, create the units input
if( this.hasUnits() ) {
// Set the units id
unitsId = inputId + '-units';
var unitsValue = this.isValueMathObject() ? value.formatUnits() : null;
// Create the units container
$unitsContainer = $( '<div>', {
class: 'input-group-append'
} );
// Initialize the units input options
var unitsInputAttributes = {
id: unitsId,
class: 'custom-select calculator-input-select'
};
if( options.size === INPUTSIZE_COMPACT ) {
unitsInputAttributes.class = unitsInputAttributes.class + ' calculator-input-select-compact';
}
unitsInputAttributes.class = unitsInputAttributes.class + ' ' + unitsId;
var $unitsInput = $( '<select>', unitsInputAttributes )
.on( 'change', function() {
var newValue = $( '#' + inputId ).val() + ' ' + $( this ).val();
mw.calculators.setValue( varId, newValue );
} );
for( var iUnits in this.units ) {
var units = this.units[ iUnits ];
var unitsOptionAttributes = {
text: units,
value: units
};
// Apply custom rendering function for units if defined
if( typeof this.renderUnits === 'function' ) {
unitsOptionAttributes.text = this.renderUnits( units );
}
if( units === unitsValue ) {
unitsOptionAttributes.selected = true;
}
$unitsInput.append( $( '<option>', unitsOptionAttributes ) );
}
$unitsContainer.append( $unitsInput );
}
// Create the input and add handlers
var $input = $( '<input>', inputAttributes )
.on( 'input', function() {
var newValue = $( this ).val();
if( unitsId ) {
newValue = newValue + ' ' + $( '#' + unitsId ).val();
}
mw.calculators.setValue( varId, newValue );
} );
// Create the input label and append to the container
$inputContainer.append( $( '<label>', labelAttributes ) );
// Create the input group
var $inputGroup = $( '<div>', {
class: 'input-group'
} ).append( $input );
if( $unitsContainer ) {
$inputGroup.append( $unitsContainer );
}
$inputContainer.append( $inputGroup );
} else if( this.getType === TYPE_STRING ) {
}
return $inputContainer;
};
CalculatorVariable.prototype.getId = function() {
return this.id;
};
CalculatorVariable.prototype.getName = function() {
return this.name;
};
CalculatorVariable.prototype.getType = function() {
return this.type;
};
CalculatorVariable.prototype.getValue = function() {
return this.value;
};
CalculatorVariable.prototype.getValueString = function() {
return String( this.value );
};
CalculatorVariable.prototype.hasOptions = function() {
return this.options !== null;
};
CalculatorVariable.prototype.hasUnits = function() {
return this.units !== null;
};
CalculatorVariable.prototype.hasValue = function() {
if( !this.value ||
( this.isValueMathObject() && !this.value.toNumber() ) ) {
return false;
}
return true;
};
CalculatorVariable.prototype.isValueMathObject = function() {
return this.value && this.value.hasOwnProperty( 'value' );
};
CalculatorVariable.prototype.setValue = function( value ) {
if( this.getType() === 'number' ) {
if( typeof value !== 'object' ) {
value = math.unit( value );
}
if( this.hasUnits() ) {
var valueUnits = value.formatUnits();
if( !valueUnits ) {
throw new Error( 'Could not set value for "' + this.getId() + '": Value must define units' );
} else if( this.units.indexOf( valueUnits ) === -1 ) {
throw new Error( 'Could not set value for "' + this.getId() + '": Units "' + valueUnits + '" are not valid for this variable' );
}
}
} else if( this.hasOptions() ) {
if( this.options.indexOf( value ) === -1 ) {
throw new Error( 'Could not set value "' + value + '" for "' + this.getId() + '": Value must define be one of: ' + this.options.join( ', ' ) );
}
}
this.value = value;
for( var iCalculation in this.calculations ) {
var calculation = mw.calculators.getCalculation( this.calculations[ iCalculation ] );
if( calculation ) {
calculation.render();
}
}
return true;
};
/**
* Class CalculatorCalculation
* @param {Object} varData
* @returns {CalculatorCalculation}
* @constructor
*/
function CalculatorCalculation( varData ) {
var propertyData = {
required: [
'id',
'name',
'calculate'
],
optional: [
'abbreviation',
'digits',
'references',
'units',
'variables'
]
};
CalculatorObject.call( this, varData, propertyData );
if( typeof this.calculate !== 'function' ) {
throw new Error( 'calculate() must be a function for Calculation "' + this.id + '"' );
}
this.variables = this.variables ? this.variables : {};
if( !this.variables.hasOwnProperty( 'required' ) ) {
this.variables.required = [];
}
if( !this.variables.hasOwnProperty( 'optional' ) ) {
this.variables.optional = [];
}
this.message = null;
this.value = null;
}
CalculatorCalculation.prototype = Object.create( CalculatorObject.prototype );
CalculatorCalculation.prototype.getId = function() {
return this.id;
};
CalculatorCalculation.prototype.recalculate = function() {
this.message = '';
var data = {};
var missingRequiredData = '';
var varId, variable;
for( var iRequiredVariable in this.variables.required ) {
varId = this.variables.required[ iRequiredVariable ];
variable = mw.calculators.getVariable( varId );
if( !variable ) {
throw new Error( 'Invalid required variable "' + varId + '" for calculation "' + this.id + '"' );
} else if( !variable.hasValue() ) {
if( missingRequiredData ) {
missingRequiredData = missingRequiredData + ', ';
}
missingRequiredData = missingRequiredData + varId;
} else {
data[ varId ] = variable.getValue();
}
}
if( missingRequiredData ) {
this.message = missingRequiredData + ' required';
return false;
}
for( var iOptionalVariable in this.variables.optional ) {
varId = this.variables.optional[ iOptionalVariable ];
variable = mw.calculators.getVariable( varId );
if( !variable ) {
throw new Error( 'Invalid optional variable "' + varId + '" for calculation "' + this.id + '"' );
}
data[ varId ] = variable.hasValue() ? variable.getValue() : null;
}
var value = this.calculate( data );
if( this.units ) {
value = value + ' ' + this.units;
}
this.value = math.unit( value );
return true;
};
CalculatorCalculation.prototype.render = function( options ) {
this.recalculate();
var labelString = this.renderLabel( options );
var valueString = this.toString();
var containerClass = 'calculator-result-' + this.getId();
var $container = $( '.' + containerClass );
if( !$container.length ) {
return;
}
var $renderOutput = null;
if( options && options.hasOwnProperty( 'format' ) && options.format === RENDERFORMAT_ROW ) {
$renderOutput = $( '<div>', {
class: 'row ' + containerClass
} );
$renderOutput.append( $( '<div>', {
class: 'col'
} ) ).append( labelString );
$renderOutput.append( $( '<div>', {
class: 'col'
} ) ).append( valueString );
} else {
$renderOutput = $( '<div>', {
class: containerClass
} ).text( labelString + ': ' + valueString );
}
console.log('rendering ' + this.id );
console.log($container);
console.log($renderOutput);
$container.replaceWith( $renderOutput );
};
CalculatorCalculation.prototype.renderLabel = function( options ) {
return options && options.hasOwnProperty( 'label' ) &&
options.label === RENDERLABEL_ABBREVIATION &&
this.abbreviation ? this.abbreviation : this.name;
};
CalculatorCalculation.prototype.toString = function() {
if( this.message ) {
return this.message;
} else if( typeof this.value === 'object' && this.value.hasOwnProperty( 'value' ) ) {
var value = this.value.toNumber( this.units );
if( this.digits !== null ) {
value = value.toFixed( this.digits );
}
if( this.units ) {
value = value + ' ' + this.value.formatUnits().replace( /\s/g, '' );
}
return value;
} else {
return String( this.value );
}
};
mw.calculators = {
calculations: {},
variables: {},
addCalculations: function( calculations ) {
for( var calcId in calculations ) {
if( mw.calculators.calculations.hasOwnProperty( calcId ) ) {
console.warn( 'Calculation variable "' + calcId + '" already defined.' );
continue;
}
var calcData = calculations[ calcId ];
calcData.id = calcId;
var calculation = new CalculatorCalculation( calcData );
if( !calculation ) {
continue;
}
mw.calculators.calculations[ calcId ] = calculation;
var variables = calculation.variables.required.concat( calculation.variables.optional );
for( var iVariable in variables ) {
console.log(variables);
var varId = variables[ iVariable ];
if( !mw.calculators.variables.hasOwnProperty( varId ) ) {
throw new Error('Variable "' + varId + '" does not exist for calculation "' + calcId + '"');
}
mw.calculators.variables[ varId ].addCalculation( calcId );
}
}
},
addVariables: function( variables ) {
for( var varId in variables ) {
if( mw.calculators.variables.hasOwnProperty( varId ) ) {
console.warn( 'Calculation variable "' + varId + '" already defined.' );
continue;
}
var varData = variables[ varId ];
varData.id = varId;
var variable = new CalculatorVariable( varData );
if( variable ) {
var cookieValue = mw.calculators.getCookieValue( varId );
if( cookieValue ) {
variable.setValue( cookieValue );
}
mw.calculators.variables[ varId ] = variable;
}
}
},
defineUnits: function() {
// Create aliases for abbreviations of existing units
math.createUnit( 'dy', {
definition: '1 day'
} );
math.createUnit( 'mo', {
definition: '1 month'
} );
math.createUnit( 'yr', {
definition: '1 year'
} );
// Body weight needs to be treated as a different fundamental unit type compared to mass for stoichiometry
// Gram-weight, which uses short SI prefixes (e.g. 1 kgwt = 1000 gwt)
math.createUnit( 'gwt', {
prefixes: 'short'
} );
// Pound-weight, which uses no prefixes
math.createUnit( 'lbwt', {
definition: '453.59237 gwt'
} );
math.createUnit( 'puff' );
math.createUnit( 'unit', {
aliases: [ 'units' ]
} );
math.createUnit( 'vial', {
aliases: [ 'vials' ]
} );
},
getCookieKey: function( varId ) {
return 'calculators-var-' + varId;
},
getCookieValue: function( varId ) {
var cookieValue = mw.cookie.get( mw.calculators.getCookieKey( varId ) );
if( !cookieValue ) {
return null;
}
return cookieValue;
},
getCalculation: function( calcId ) {
if( mw.calculators.calculations.hasOwnProperty( calcId ) ) {
return mw.calculators.calculations[ calcId ];
} else {
return null;
}
},
getVariable: function( varId ) {
if( mw.calculators.variables.hasOwnProperty( varId ) ) {
return mw.calculators.variables[ varId ];
} else {
return null;
}
},
getValue: function( varId ) {
if( mw.calculators.variables.hasOwnProperty( varId ) ) {
return mw.calculators.variables[ varId ].getValue();
} else {
return null;
}
},
init: function() {
mw.calculators.defineUnits();
},
setValue: function( varId, value ) {
if( !mw.calculators.variables.hasOwnProperty( varId ) ) {
return false;
}
if( mw.calculators.variables[ varId ].setValue( value ) ) {
mw.cookie.set( mw.calculators.getCookieKey( varId ), value, {
expires: COOKIE_EXPIRATION
} );
return true;
}
return false;
}
};
mw.calculators.init();
}() );