MediaWiki:Gadget-calculator-drugs-core.js
From WikiAnesthesia
Revision as of 10:48, 11 August 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() {
var COOKIE_EXPIRATION = 12 * 60 * 60;
var TYPE_NUMBER = 'number';
var TYPE_STRING = 'string';
var VALID_TYPES = [
TYPE_NUMBER,
TYPE_STRING
];
var DEFAULT_CALCULATION_CLASS = 'SimpleCalculation';
var DEFAULT_CALCULATOR_CLASS = 'SimpleCalculator';
// Polyfill to fetch unit's base. This may become unnecessary in a future version of math.js
math.Unit.prototype.getBase = function() {
for( var iBase in math.Unit.BASE_UNITS ) {
if( this.equalBase( math.Unit.BASE_UNITS[ iBase ] ) ) {
return iBase;
}
}
return null;
};
mw.calculators = {
calculators: {},
calculations: {},
objectClasses: {},
units: {},
unitsBases: {},
variables: {},
addCalculations: function( calculationData, className ) {
className = className ? className : DEFAULT_CALCULATION_CLASS;
var calculations = mw.calculators.createCalculatorObjects( className, calculationData );
for( var calculationId in calculations ) {
var calculation = calculations[ calculationId ];
mw.calculators.calculations[ calculationId ] = calculation;
mw.calculators.calculations[ calculationId ].setDependencies();
mw.calculators.calculations[ calculationId ].recalculate();
}
},
addCalculators: function( moduleId, calculatorData, className ) {
className = className ? className : DEFAULT_CALCULATOR_CLASS;
for( var calculatorId in calculatorData ) {
calculatorData[ calculatorId ].module = moduleId;
}
var calculators = mw.calculators.createCalculatorObjects( className, calculatorData );
if( !mw.calculators.calculators.hasOwnProperty( moduleId ) ) {
mw.calculators.calculators[ moduleId ] = {};
}
for( var calculatorId in calculators ) {
mw.calculators.calculators[ moduleId ][ calculatorId ] = calculators[ calculatorId ];
mw.calculators.calculators[ moduleId ][ calculatorId ].render();
}
},
addUnitsBases: function( unitsBaseData ) {
var unitsBases = mw.calculators.createCalculatorObjects( 'UnitsBase', unitsBaseData );
for( var unitsBaseId in unitsBases ) {
mw.calculators.unitsBases[ unitsBaseId ] = unitsBases[ unitsBaseId ];
}
},
addUnits: function( unitsData ) {
var units = mw.calculators.createCalculatorObjects( 'Units', unitsData );
for( var unitsId in units ) {
if( mw.calculators.units.hasOwnProperty( unitsId ) ) {
continue;
}
try {
math.createUnit( unitsId, {
aliases: units[ unitsId ].aliases,
baseName: units[ unitsId ].baseName,
definition: units[ unitsId ].definition,
prefixes: units[ unitsId ].prefixes,
offset: units[ unitsId ].offset,
} );
} catch( e ) {
console.warn( e.message );
}
mw.calculators.units[ units ] = units[ unitsId ];
}
},
addVariables: function( variableData ) {
var variables = mw.calculators.createCalculatorObjects( 'Variable', variableData );
for( var varId in variables ) {
var variable = variables[ varId ];
var cookieValue = mw.calculators.getCookieValue( varId );
if( cookieValue ) {
variable.setValue( cookieValue );
}
mw.calculators.variables[ varId ] = variable;
}
},
createCalculatorObjects: function( className, objectData ) {
if( !mw.calculators.objectClasses.hasOwnProperty( className ) ) {
throw new Error( 'Invalid class name "' + className + '"' );
}
var objects = {};
for( var objectId in objectData ) {
var propertyValues = objectData[ objectId ];
if( typeof objectId === 'string' ) {
propertyValues.id = objectId;
}
objects[ objectId ] = new mw.calculators.objectClasses[ className ]( propertyValues );
}
return objects;
},
createInputGroup: function( variableIds ) {
var $form = $( '<form>', {
} );
var $formRow = $( '<div>', {
class: 'form-row'
} ).css( 'flex-wrap', 'nowrap' );
for( var iVariableId in variableIds ) {
var variableId = variableIds[ iVariableId ];
if( !mw.calculators.variables.hasOwnProperty( variableId ) ) {
throw new Error( 'Invalid variable name "' + variableId + '"' );
}
$formRow.append( mw.calculators.variables[ variableId ].createInput() );
}
return $form.append( $formRow );
},
getCookieKey: function( variableId ) {
return 'calculators-var-' + variableId;
},
getCookieValue: function( varId ) {
var cookieValue = mw.cookie.get( mw.calculators.getCookieKey( varId ) );
if( !cookieValue ) {
return null;
}
return cookieValue;
},
getCalculation: function( calculationId ) {
if( mw.calculators.calculations.hasOwnProperty( calculationId ) ) {
return mw.calculators.calculations[ calculationId ];
} else {
return null;
}
},
getCalculator: function( moduleId, calculatorId ) {
if( mw.calculators.calculators.hasOwnProperty( moduleId ) &&
mw.calculators.calculators[ moduleId ].hasOwnProperty( calculatorId ) ) {
return mw.calculators.calculators[ moduleId ][ calculatorId ];
} else {
return null;
}
},
getUnitsString: function( value ) {
if( typeof value !== 'object' ) {
return null;
}
var unitsString = value.formatUnits();
var reDenominator = /\/\s?\((.*)\)/;
var denominatorMatches = unitsString.match( reDenominator );
if( denominatorMatches ) {
var denominatorUnits = denominatorMatches[ 1 ];
unitsString = unitsString.replace( reDenominator, '/' + denominatorUnits.replace( ' ', '/' ) );
}
unitsString = unitsString
.replace( /\s/g, '' )
.replace( /(\^(\d+))/g, '<sup>$2</sup>' );
var unitsBase = value.getBase();
if( unitsBase ) {
if( mw.calculators.unitsBases.hasOwnProperty( unitsBase ) &&
typeof mw.calculators.unitsBases[ unitsBase ].toString === 'function' ) {
unitsString = mw.calculators.unitsBases[ unitsBase ].toString( unitsString );
}
} else {
// TODO nasty hack to fix weight units in compound units which have no base
unitsString = unitsString.replace( 'kgwt', 'kg' );
unitsString = unitsString.replace( 'ug', 'mcg' );
}
return unitsString;
},
getValueString: function( value ) {
if( typeof value !== 'object' ) {
return null;
}
var valueString = String( value.toNumber() );
if( value.formatUnits() ) {
valueString += ' ' + mw.calculators.getUnitsString( value );
}
return valueString;
},
getVariable: function( variableId ) {
if( mw.calculators.variables.hasOwnProperty( variableId ) ) {
return mw.calculators.variables[ variableId ];
} else {
return null;
}
},
initialize: function() {
math.config( {
number: 'BigNumber'
} );
$( '.calculator' ).each( function() {
var gadgetModule = 'ext.gadget.calculator-' + $( this ).attr( 'data-module' );
if( gadgetModule && mw.loader.getState( gadgetModule ) === 'registered' ) {
mw.loader.load( gadgetModule );
}
} );
},
isMobile: function() {
return window.matchMedia( 'only screen and (max-width: 760px)' ).matches;
},
isValueMathObject: function( value ) {
return value && value.hasOwnProperty( 'value' );
},
setValue: function( variableId, value ) {
if( !mw.calculators.variables.hasOwnProperty( variableId ) ) {
return false;
}
if( mw.calculators.variables[ variableId ].setValue( value ) ) {
mw.cookie.set( mw.calculators.getCookieKey( variableId ), value, {
expires: COOKIE_EXPIRATION
} );
return true;
}
return false;
},
uniqueValues: function( value, index, self ) {
return self.indexOf( value ) === index;
}
};
/**
* Class CalculatorObject
*
* @param {Object} properties
* @param {Object} propertyValues
* @returns {mw.calculators.objectClasses.CalculatorObject}
* @constructor
*/
mw.calculators.objectClasses.CalculatorObject = function( properties, propertyValues ) {
propertyValues = propertyValues ? propertyValues : {};
if( properties ) {
if( properties.hasOwnProperty( 'required' ) ) {
for( var iRequiredProperty in properties.required ) {
var requiredProperty = properties.required[ iRequiredProperty ];
if( !propertyValues || !propertyValues.hasOwnProperty( requiredProperty ) ) {
console.error( 'Missing required property "' + requiredProperty + '"' );
console.log( propertyValues );
return null;
}
this[ requiredProperty ] = propertyValues[ requiredProperty ];
delete propertyValues[ requiredProperty ];
}
}
if( properties.hasOwnProperty( 'optional' ) ) {
for( var iOptionalProperty in properties.optional ) {
var optionalProperty = properties.optional[ iOptionalProperty ];
if( propertyValues && propertyValues.hasOwnProperty( optionalProperty ) ) {
this[ optionalProperty ] = propertyValues[ optionalProperty ];
delete propertyValues[ optionalProperty ];
} else if( typeof this[ optionalProperty ] === 'undefined' ) {
this[ optionalProperty ] = null;
}
}
}
var invalidProperties = Object.keys( propertyValues );
if( invalidProperties.length ) {
console.warn( 'Unsupported properties defined for ' + typeof this + ' with id "' + this.id + '": ' + invalidProperties.join( ', ' ) );
}
}
};
mw.calculators.objectClasses.CalculatorObject.prototype.getProperties = function() {
return {
required: [],
optional: []
};
};
mw.calculators.objectClasses.CalculatorObject.prototype.mergeProperties = function( inheritedProperties, properties ) {
var uniqueValues = function( value, index, self ) {
return self.indexOf( value ) === index;
};
properties.required = inheritedProperties.required.concat( properties.required ).filter( uniqueValues );
properties.optional = inheritedProperties.optional.concat( properties.optional ).filter( uniqueValues );
return properties;
};
/**
* Class UnitsBase
* @param {Object} propertyValues
* @returns {mw.calculators.objectClasses.UnitsBase}
* @constructor
*/
mw.calculators.objectClasses.UnitsBase = function( propertyValues ) {
var properties = {
required: [
'id'
],
optional: [
'toString'
]
};
mw.calculators.objectClasses.CalculatorObject.call( this, properties, propertyValues );
};
mw.calculators.objectClasses.UnitsBase.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
/**
* Class Units
* @param {Object} propertyValues
* @returns {mw.calculators.objectClasses.Units}
* @constructor
*/
mw.calculators.objectClasses.Units = function( propertyValues ) {
var properties = {
required: [
'id'
],
optional: [
'aliases',
'baseName',
'definition',
'offset',
'prefixes'
]
};
mw.calculators.objectClasses.CalculatorObject.call( this, properties, propertyValues );
};
mw.calculators.objectClasses.Units.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
/**
* Class Variable
* @param {Object} propertyValues
* @returns {mw.calculators.objectClasses.Variable}
* @constructor
*/
mw.calculators.objectClasses.Variable = function( propertyValues ) {
var properties = {
required: [
'id',
'name',
'type'
],
optional: [
'abbreviation',
'defaultValue',
'maxLength',
'options',
'units'
]
};
mw.calculators.objectClasses.CalculatorObject.call( this, properties, propertyValues );
if( VALID_TYPES.indexOf( this.type ) === -1 ) {
throw new Error( 'Invalid type "' + this.type + '" for variable "' + this.id + '"' );
}
// Accept options as either an array of strings, or an object with ids as keys and display text as values
if( Array.isArray( this.options ) ) {
var options = {};
for( var iOption in this.options ) {
var option = this.options[ iOption ];
options[ option ] = option;
}
this.options = options;
}
this.calculations = [];
if( this.defaultValue ) {
this.setValue( this.defaultValue );
} else {
this.value = null;
}
};
mw.calculators.objectClasses.Variable.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
mw.calculators.objectClasses.Variable.prototype.addCalculation = function( calculationId ) {
if( this.calculations.indexOf( calculationId ) !== -1 ) {
return;
}
this.calculations.push( calculationId );
};
mw.calculators.objectClasses.Variable.prototype.createInput = function( hideLabel ) {
var variableId = this.id;
var inputContainerAttribs = {
class: 'form-group mb-0 calculator-container-input'
};
inputContainerAttribs.class = inputContainerAttribs.class + ' calculator-container-input-' + variableId;
// Create the input container
var $inputContainer = $( '<div>', inputContainerAttribs );
// Set the input id
var inputId = 'calculator-input-' + variableId;
// Initialize label attributes
var labelAttributes = {
for: inputId,
text: this.getLabelString()
};
if( hideLabel ) {
labelAttributes.class = 'sr-only';
}
// Create the input label and append to the container
$inputContainer.append( $( '<label>', labelAttributes ) );
if( this.type === TYPE_NUMBER ) {
// Initialize the primary units variables (needed for handlers, even if doesn't have units)
var unitsId = null;
var $unitsContainer = null;
// Initialize input options
var inputAttributes = {
id: inputId,
class: 'form-control calculator-input-text',
type: 'text',
autocomplete: 'off',
inputmode: 'decimal',
value: this.isValueMathObject() ? this.value.toNumber() : this.value
};
// Configure additional options
if( this.maxLength ) {
inputAttributes.maxlength = this.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() ? this.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'
};
unitsInputAttributes.class = unitsInputAttributes.class + ' ' + unitsId;
var $unitsInput = $( '<select>', unitsInputAttributes )
.on( 'change', function() {
var newValue = $( '#' + inputId ).val() + ' ' + $( this ).val();
mw.calculators.setValue( variableId, newValue );
} );
for( var iUnits in this.units ) {
var units = this.units[ iUnits ];
var unitsOptionAttributes = {
text: mw.calculators.getUnitsString( math.unit( '0 ' + units ) ),
value: 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( variableId, newValue );
} );
// Create the input group
var $inputGroup = $( '<div>', {
class: 'input-group'
} ).append( $input );
if( $unitsContainer ) {
$inputGroup.append( $unitsContainer );
}
$inputContainer.append( $inputGroup );
} else if( this.type === TYPE_STRING ) {
if( this.hasOptions() ) {
var selectAttributes = {
id: inputId,
class: 'custom-select calculator-input-select'
};
var $select = $( '<select>', selectAttributes )
.on( 'change', function() {
mw.calculators.setValue( variableId, $( this ).val() );
} );
for( var optionId in this.options ) {
var displayText = this.options[ optionId ];
var optionAttributes = {
value: optionId,
text: displayText
};
if( optionId === this.value ) {
optionAttributes.selected = true;
}
$select.append( $( '<option>', optionAttributes ) );
}
$inputContainer.append( $select );
}
}
return $inputContainer;
};
mw.calculators.objectClasses.Variable.prototype.getLabelString = function() {
return mw.calculators.isMobile() && this.abbreviation ? this.abbreviation : this.name;
};
mw.calculators.objectClasses.Variable.prototype.getValueString = function() {
return String( this.value );
};
mw.calculators.objectClasses.Variable.prototype.hasOptions = function() {
return this.options !== null;
};
mw.calculators.objectClasses.Variable.prototype.hasUnits = function() {
return this.units !== null;
};
mw.calculators.objectClasses.Variable.prototype.hasValue = function() {
if( !this.value ||
( this.isValueMathObject() && !this.value.toNumber() ) ) {
return false;
}
return true;
};
mw.calculators.objectClasses.Variable.prototype.isValueMathObject = function() {
return mw.calculators.isValueMathObject( this.value );
};
mw.calculators.objectClasses.Variable.prototype.setValue = function( value ) {
if( this.type === TYPE_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.id + '": Value must define units' );
} else if( this.units.indexOf( valueUnits ) === -1 ) {
throw new Error( 'Could not set value for "' + this.id + '": Units "' + valueUnits + '" are not valid for this variable' );
}
}
} else if( this.hasOptions() ) {
if( !this.options.hasOwnProperty( value ) ) {
throw new Error( 'Could not set value "' + value + '" for "' + this.id + '": Value must define be one of: ' + Object.keys( 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 AbstractCalculation
* @param {Object} propertyValues
* @returns {mw.calculators.objectClasses.AbstractCalculation}
* @constructor
*/
mw.calculators.objectClasses.AbstractCalculation = function( propertyValues ) {
mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
this.initialize();
};
mw.calculators.objectClasses.AbstractCalculation.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
mw.calculators.objectClasses.AbstractCalculation.prototype.addCalculation = function( calculationId ) {
if( this.calculations.indexOf( calculationId ) !== -1 ) {
return;
}
this.calculations.push( calculationId );
};
mw.calculators.objectClasses.AbstractCalculation.prototype.getContainerClass = function() {
return 'calculator-calculation-' + this.id;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.getLabelString = function() {
return this.id;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.getProperties = function() {
return {
required: [
'id',
'calculate'
],
optional: [
'data',
'description',
'onRender',
'onRendered',
'references',
'type'
]
};
};
mw.calculators.objectClasses.AbstractCalculation.prototype.getValue = function() {
// For now, we always need to recalculate, since the calculation may not be rendered but still required by
// other calculations (i.e. drug dosages using lean body weight).
this.recalculate();
return this.value;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.hasInfo = function() {
return false;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.hasValue = function() {
if( !this.value ||
( this.isValueMathObject() && !this.value.toNumber() ) ) {
return false;
}
return true;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.getCalculationData = function() {
return this.data;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.getCalculationDataValues = function() {
var calculationData = this.getCalculationData();
var data = {};
var missingRequiredData = '';
var calculationId, calculation, variableId, variable;
for( var iRequiredCalculation in calculationData.calculations.required ) {
calculationId = calculationData.calculations.required[ iRequiredCalculation ];
calculation = mw.calculators.getCalculation( calculationId );
if( !calculation ) {
throw new Error( 'Invalid required calculation "' + calculationId + '" for calculation "' + this.id + '"' );
} else if( !calculation.hasValue() ) {
if( missingRequiredData ) {
missingRequiredData = missingRequiredData + ', ';
}
missingRequiredData = missingRequiredData + calculation.getLabelString();
} else {
data[ calculationId ] = calculation.value;
}
}
for( var iRequiredVariable in calculationData.variables.required ) {
variableId = calculationData.variables.required[ iRequiredVariable ];
variable = mw.calculators.getVariable( variableId );
if( !variable ) {
throw new Error( 'Invalid required variable "' + variableId + '" for calculation "' + this.id + '"' );
} else if( !variable.hasValue() ) {
if( missingRequiredData ) {
missingRequiredData = missingRequiredData + ', ';
}
missingRequiredData = missingRequiredData + variable.getLabelString();
} else {
data[ variableId ] = variable.value;
}
}
if( missingRequiredData ) {
this.message = missingRequiredData + ' required';
return false;
}
for( var iOptionalCalculation in calculationData.calculations.optional ) {
calculationId = calculationData.calculations.optional[ iOptionalCalculation ];
calculation = mw.calculators.getCalculation( calculationId );
if( !calculation ) {
throw new Error( 'Invalid optional calculation "' + calculationId + '" for calculation "' + this.id + '"' );
}
data[ calculationId ] = calculation.hasValue() ? calculation.value : null;
}
for( var iOptionalVariable in calculationData.variables.optional ) {
variableId = calculationData.variables.optional[ iOptionalVariable ];
variable = mw.calculators.getVariable( variableId );
if( !variable ) {
throw new Error( 'Invalid optional variable "' + variableId + '" for calculation "' + this.id + '"' );
}
data[ variableId ] = variable.hasValue() ? variable.value : null;
}
return data;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.initialize = function() {
if( typeof this.calculate !== 'function' ) {
throw new Error( 'calculate() must be a function for Calculation "' + this.id + '"' );
}
// Initialize array to store calculation ids which depend on this calculation's value
this.calculations = [];
this.data = new mw.calculators.objectClasses.CalculationData( this.data );
this.type = this.type ? this.type : TYPE_NUMBER;
this.message = null;
this.value = null;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.isValueMathObject = function() {
return mw.calculators.isValueMathObject( this.value );
};
mw.calculators.objectClasses.AbstractCalculation.prototype.recalculate = function() {
this.message = '';
this.value = null;
var data = this.getCalculationDataValues();
if( data === false ) {
return false;
}
try {
var value = this.calculate( data );
if( this.type === TYPE_NUMBER && !isNaN( value ) ) {
if( this.units ) {
value = value + ' ' + this.units;
}
this.value = math.unit( value );
} else {
this.value = value;
}
for( var iCalculation in this.calculations ) {
calculation = mw.calculators.getCalculation( this.calculations[ iCalculation ] );
if( calculation ) {
calculation.render();
}
}
} catch( e ) {
this.message = e.message;
this.value = null;
}
return true;
};
mw.calculators.objectClasses.AbstractCalculation.prototype.render = function() {
this.recalculate();
if( typeof this.onRender === 'function' ) {
this.onRender();
}
this.doRender();
if( typeof this.onRendered === 'function' ) {
this.onRendered();
}
};
mw.calculators.objectClasses.AbstractCalculation.prototype.setDependencies = function() {
var calculationData = this.getCalculationData();
var calculationIds = calculationData.calculations.required.concat( calculationData.calculations.optional );
for( var iCalculationId in calculationIds ) {
var calculationId = calculationIds[ iCalculationId ];
if( !mw.calculators.calculations.hasOwnProperty( calculationId ) ) {
throw new Error('Calculation "' + calculationId + '" does not exist for calculation "' + this.id + '"');
}
mw.calculators.calculations[ calculationId ].addCalculation( this.id );
}
var variableIds = calculationData.variables.required.concat( calculationData.variables.optional );
for( var iVariableId in variableIds ) {
var variableId = variableIds[ iVariableId ];
if( !mw.calculators.variables.hasOwnProperty( variableId ) ) {
throw new Error('Variable "' + variableId + '" does not exist for calculation "' + this.id + '"');
}
mw.calculators.variables[ variableId ].addCalculation( this.id );
}
};
mw.calculators.objectClasses.AbstractCalculation.prototype.doRender = function() {};
/**
* Class CalculationData
* @param {Object} propertyValues
* @returns {mw.calculators.objectClasses.CalculationData}
* @constructor
*/
mw.calculators.objectClasses.CalculationData = function( propertyValues ) {
mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
var dataTypes = this.getDataTypes();
for( var iDataType in dataTypes ) {
var dataType = dataTypes[ iDataType ];
if( !this[ dataType ] ) {
this[ dataType ] = {
optional: [],
required: []
};
} else {
this[ dataType ].optional = this[ dataType ].hasOwnProperty( 'optional' ) ? this[ dataType ].optional : [];
this[ dataType ].required = this[ dataType ].hasOwnProperty( 'required' ) ? this[ dataType ].required : [];
}
}
};
mw.calculators.objectClasses.CalculationData.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
mw.calculators.objectClasses.CalculationData.prototype.getDataTypes = function() {
return [
'calculations',
'variables'
];
};
mw.calculators.objectClasses.CalculationData.prototype.getProperties = function() {
return {
required: [],
optional: [
'calculations',
'variables'
]
};
};
mw.calculators.objectClasses.CalculationData.prototype.merge = function() {
var mergedData = new mw.calculators.objectClasses.CalculationData();
var data = [ this ].concat( Array.prototype.slice.call( arguments ) );
var dataTypes = this.getDataTypes();
for( var iData in data ) {
for( var iDataType in dataTypes ) {
var dataType = dataTypes[ iDataType ];
mergedData[ dataType ].required = mergedData[ dataType ].required
.concat( data[ iData ][ dataType ].required )
.filter( mw.calculators.uniqueValues );
mergedData[ dataType ].optional = mergedData[ dataType ].optional
.concat( data[ iData ][ dataType ].optional )
.filter( mw.calculators.uniqueValues );
}
}
return mergedData;
};
/**
* Class SimpleCalculation
* @param {Object} propertyValues
* @returns {mw.calculators.objectClasses.SimpleCalculation}
* @constructor
*/
mw.calculators.objectClasses.SimpleCalculation = function( propertyValues ) {
mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
this.initialize();
};
mw.calculators.objectClasses.SimpleCalculation.prototype = Object.create( mw.calculators.objectClasses.AbstractCalculation.prototype );
mw.calculators.objectClasses.SimpleCalculation.prototype.hasInfo = function() {
return this.description || this.formula || this.references.length;
};
mw.calculators.objectClasses.SimpleCalculation.prototype.getLabelHtml = function() {
var labelHtml = this.getLabelString();
if( this.link ) {
var href = this.link;
// Detect internal links (this isn't great)
var matches = href.match( /\[\[(.*?)\]\]/ );
if( matches ) {
href = mw.util.getUrl( matches[ 1 ] );
}
labelHtml = $( '<a>', {
href: href,
text: labelHtml
} )[ 0 ].outerHTML;
}
return labelHtml;
};
mw.calculators.objectClasses.SimpleCalculation.prototype.getLabelString = function() {
return mw.calculators.isMobile() && this.abbreviation ? this.abbreviation : this.name;
};
mw.calculators.objectClasses.SimpleCalculation.prototype.getProperties = function() {
var inheritedProperties = mw.calculators.objectClasses.AbstractCalculation.prototype.getProperties();
return this.mergeProperties( inheritedProperties, {
required: [
'name'
],
optional: [
'abbreviation',
'digits',
'formula',
'link',
'units'
]
} );
};
mw.calculators.objectClasses.SimpleCalculation.prototype.getValueString = function() {
if( this.message ) {
return this.message;
} else if( typeof this.value === 'object' && this.value.hasOwnProperty( 'value' ) ) {
// format() will convert the value to the most visually appealing units (e.g. 5200 mL becomes 5.2 L)
// We then want to turn that back into a math object.
var value = math.unit( this.value.format() );
var units = value.formatUnits();
var number = value.toNumber();
var digits = ( this.value.formatUnits() === units && this.digits !== null ) ? this.digits : 1;
var valueString = String( number.toFixed( digits ) );
if( units ) {
valueString = valueString + ' ' + mw.calculators.getUnitsString( value );
}
return valueString;
} else {
return String( this.value );
}
};
mw.calculators.objectClasses.SimpleCalculation.prototype.doRender = function() {
var $calculationContainer = $( '.' + this.getContainerClass() );
if( !$calculationContainer.length ) {
return;
}
var valueString = this.getValueString();
var inputVariableIds = this.data.variables.required.concat( this.data.variables.optional );
var missingVariableInputs = [];
for( var iInputVariableId in inputVariableIds ) {
var variableId = inputVariableIds[ iInputVariableId ];
if( !$( '#calculator-input-' + variableId ).length ) {
missingVariableInputs.push( variableId );
}
}
var calculation = this;
$calculationContainer.each( function() {
$( this ).empty();
var isTable = this.tagName.toLowerCase() === 'tr';
var $infoButton = null;
if( calculation.hasInfo() ) {
$infoButton = $( '<a>', {
'data-toggle': 'collapse',
href: '#' + calculation.getContainerClass() + '-info',
role: 'button',
'aria-expanded': 'false',
'aria-controls': calculation.getContainerClass() + '-info'
} )
.append( $( '<i>', {
class: 'far fa-question-circle'
} ) );
}
var labelHtml = calculation.getLabelHtml();
if( isTable ) {
if( calculation.hasInfo() ) {
labelHtml += $( '<span>', {
class: 'calculator-calculation-column-label-info'
} ).append( $infoButton )[ 0 ].outerHTML;
}
$( this )
.append( $( '<th>', {
html: labelHtml
} ) )
.append( $( '<td>', {
class: 'calculator-calculation-column-value',
html: valueString
} ) );
} else {
$( this )
.append( labelHtml + $infoButton[ 0 ].outerHTML + ': ' + valueString );
}
if( calculation.hasInfo() ) {
var infoHtml = '';
if( calculation.description ) {
infoHtml += $( '<p>', {
html: calculation.description
} )[ 0 ].outerHTML;
}
if( calculation.formula ) {
infoHtml += $( '<span>', {
class: calculation.getContainerClass() + '-formula'
} )[ 0 ].outerHTML;
var api = new mw.Api();
api.parse( calculation.formula ).then( function( result ) {
$( '.' + calculation.getContainerClass() + '-formula' ).html( result );
} );
}
if( calculation.references.length ) {
var $references = $( '<ol>' );
for( var iReference in calculation.references ) {
$references.append( $( '<li>', {
text: calculation.references[ iReference ]
} ) );
}
infoHtml += $references[ 0 ].outerHTML;
}
var infoContainerId = calculation.getContainerClass() + '-info';
var $infoContainer = $( '#' + infoContainerId );
if( $infoContainer.length ) {
$infoContainer.empty();
}
if( isTable ) {
$infoContainer = $( '<tr>', {
id: infoContainerId,
class: 'collapse'
} )
.append( $( '<td>', {
colspan: 2
} ).append( infoHtml ) );
} else {
$infoContainer = $( '<div>', {
id: infoContainerId,
class: 'collapse'
} ).append( infoHtml );
}
$( this ).after( $infoContainer );
}
if( missingVariableInputs.length ) {
var variablesContainerClass = 'calculator-calculation-variables ' + calculation.getContainerClass() + '-variables';
var inputGroup = mw.calculators.createInputGroup( missingVariableInputs );
if( isTable ) {
$variablesContainer = $( '<tr>' )
.append( $( '<td>', {
class: variablesContainerClass,
colspan: 2
} ).append( inputGroup ) );
} else {
$variablesContainer = $( '<div>', {
class: variablesContainerClass
} ).append( inputGroup );
}
$( this ).after( $variablesContainer );
missingVariableInputs = [];
}
} );
};
/**
* Class AbstractCalculator
* @param {Object} propertyValues
* @returns {mw.calculators.objectClasses.AbstractCalculator}
* @constructor
*/
mw.calculators.objectClasses.AbstractCalculator = function( propertyValues ) {
mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
};
mw.calculators.objectClasses.AbstractCalculator.prototype = Object.create( mw.calculators.objectClasses.CalculatorObject.prototype );
mw.calculators.objectClasses.AbstractCalculator.prototype.getContainerClass = function() {
return 'calculator-' + this.module + '-' + this.id;
};
mw.calculators.objectClasses.AbstractCalculator.prototype.getProperties = function() {
return {
required: [
'id',
'module',
'name',
'calculations'
],
optional: [
'onRender',
'onRendered'
]
};
};
mw.calculators.objectClasses.AbstractCalculator.prototype.render = function() {
if( typeof this.onRender === 'function' ) {
this.onRender();
}
this.doRender();
if( typeof this.onRendered === 'function' ) {
this.onRendered();
}
};
mw.calculators.objectClasses.AbstractCalculator.prototype.doRender = function() {};
/**
* Class SimpleCalculator
* @param {Object} propertyValues
* @returns {mw.calculators.objectClasses.SimpleCalculator}
* @constructor
*/
mw.calculators.objectClasses.SimpleCalculator = function( propertyValues ) {
mw.calculators.objectClasses.CalculatorObject.call( this, this.getProperties(), propertyValues );
};
mw.calculators.objectClasses.SimpleCalculator.prototype = Object.create( mw.calculators.objectClasses.AbstractCalculator.prototype );
mw.calculators.objectClasses.SimpleCalculator.prototype.getProperties = function() {
var inheritedProperties = mw.calculators.objectClasses.AbstractCalculator.prototype.getProperties();
return this.mergeProperties( inheritedProperties, {
required: [],
optional: [
'css',
'table'
]
} );
};
mw.calculators.objectClasses.SimpleCalculator.prototype.doRender = function() {
var $calculatorContainer = $( '.' + this.getContainerClass() );
if( !$calculatorContainer.length ) {
return;
}
if( this.css ) {
$calculatorContainer.css( this.css );
}
$calculatorContainer.empty();
$calculatorContainer.append( $( '<h4>', {
text: this.name
} ) );
var $calculationsContainer;
if( this.table ) {
$calculationsContainer = $( '<table>', {
class: 'wikitable'
} ).append( '<tbody>' );
} else {
$calculationsContainer = $( '<div>' );
}
$calculatorContainer.append( $calculationsContainer );
for( var iCalculationId in this.calculations ) {
var calculation = mw.calculators.getCalculation( this.calculations[ iCalculationId ] );
var calculationContainerClass = calculation.getContainerClass();
var $calculationContainer = $( '.' + calculationContainerClass );
// If a container doesn't exist yet, add it
if( !$calculationContainer.length ) {
if( this.table ) {
$calculationContainer = $( '<tr>', {
class: calculationContainerClass
} );
} else {
$calculationContainer = $( '<div>', {
class: calculationContainerClass
} );
}
$calculationsContainer.append( $calculationContainer );
}
calculation.render();
}
};
mw.calculators.initialize();
}() );