diff --git a/src/data/UsStates.js b/src/data/UsStates.js new file mode 100644 index 0000000..4221bf6 --- /dev/null +++ b/src/data/UsStates.js @@ -0,0 +1,101 @@ +/** + * List of US states and codes + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +/** + * List of states and their codes + * @var {Object} + */ +var states = { + 'AL': 'Alabama', + 'AK': 'Alaska', + 'AZ': 'Arizona', + 'AR': 'Arkansas', + 'CA': 'California', + 'CO': 'Colorado', + 'CT': 'Connecticut', + 'DE': 'Delaware', + 'FL': 'Florida', + 'GA': 'Georgia', + 'HI': 'Hawaii', + 'ID': 'Idaho', + 'IL': 'Illinois', + 'IN': 'Indiana', + 'IA': 'Iowa', + 'KS': 'Kansas', + 'KY': 'Kentucky', + 'LA': 'Louisiana', + 'ME': 'Maine', + 'MD': 'Maryland', + 'MA': 'Massachusetts', + 'MI': 'Michigan', + 'MN': 'Minnesota', + 'MS': 'Mississippi', + 'MO': 'Missouri', + 'MT': 'Montana', + 'NE': 'Nebraska', + 'NV': 'Nevada', + 'NH': 'New Hampshire', + 'NJ': 'New Jersey', + 'NM': 'New Mexico', + 'NY': 'New York', + 'NC': 'North Carolina', + 'ND': 'North Dakota', + 'OH': 'Ohio', + 'OK': 'Oklahoma', + 'OR': 'Oregon', + 'PA': 'Pennsylvania', + 'RI': 'Rhode Island', + 'SC': 'South Carolina', + 'SD': 'South Dakota', + 'TN': 'Tennessee', + 'TX': 'Texas', + 'UT': 'Utah', + 'VT': 'Vermont', + 'VA': 'Virginia', + 'WA': 'Washington', + 'WV': 'West Virginia', + 'WI': 'Wisconsin', + 'WY': 'Wyoming', + + // nothing + '' : '', + 0: '' +}; + + +/** + * Gets the name of the state associated with the given code + * + * @param {string} code state abbr + * + * @return {string} name of state + */ +exports.getName = function( code ) +{ + var val = states[ code ]; + + // if the value was not found, return the code we were given (which likely + // makes no sense at all, since it's not a valid state) + return ( val === undefined ) + ? code + : val; +} diff --git a/src/sort/MultiSort.js b/src/sort/MultiSort.js index 37c7d35..943a981 100644 --- a/src/sort/MultiSort.js +++ b/src/sort/MultiSort.js @@ -22,7 +22,6 @@ * - References to "quote" should be replaced with generic terminology * representing a document. * - Dependencies need to be liberated: - * - ElementStyler; * - BucketDataValidator. * - Global references (e.g. jQuery) must be removed. * - Checkbox-specific logic must be extracted. diff --git a/src/ui/ElementStyler.js b/src/ui/ElementStyler.js new file mode 100644 index 0000000..10b20b5 --- /dev/null +++ b/src/ui/ElementStyler.js @@ -0,0 +1,1314 @@ +/** + * Archaic DOM element styler + * + * Copyright (C) 2016 LoVullo Associates, Inc. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @needsLove + * - Everything! This class exists from when the framework was barely + * more than a few prototypes and has rotted ever since with little else + * but workarounds. + * @end needsLove + */ + +var State = require( '../data/UsStates' ), + Class = require( 'easejs' ).Class; + + +/** + * Styles DOM elements + * + * This class styles DOM elements with Dojo's Dijits (widgets). + * + * @return void + */ +module.exports = Class( 'ElementStyler', +{ + /** + * Stores element types as an associative array + * @type {Object.} + */ + elementTypes: {}, + + 'private _answerRefs': {}, + + /** + * Stores help text for elements + * @type {Object.} + */ + elementHelp: {}, + + /** + * Whether to show internal questions + * @type {boolean} + */ + _showInternal: false, + + /** + * Stores defaults for questions + * @type {Object} + */ + 'private _defaults': {}, + + /** + * Stores defaults for display only + * @type {Object} + */ + 'private _displayDefaults': {}, + + /** + * Contains data for select + * @type {Object.} + */ + 'private _selectData': {}, + + /** + * Selector context + * @type {jQuery} + */ + 'private _$context': null, + + + _answerStyles: { + 'currency': function( value ) + { + var pre = '$'; + + if ( !value ) + { + value = 0; + } + else if ( value < 0 ) + { + value *= -1; + pre = '-$'; + } + + var formatter = this._getAnswerStyler( 'number' ); + + return pre + formatter.call( this, ( +value ).toFixed( 2 ) ); + }, + + 'dollars': function( value ) + { + var formatter = this._getAnswerStyler( 'currency' ); + + return formatter.call( this, ( +value ).toFixed( 0 ) ); + }, + + 'float': function( value ) + { + if ( !value ) + { + value = 0; + } + + var formatter = this._getAnswerStyler( 'number' ); + + return formatter.call( this, ( +value ).toFixed( 2 ) ); + }, + + 'limit': function( value, _, default_val ) + { + value = ( value + '' ).replace( ',', '' ); + + + // if no value was given, be sure that we use the proper default + // value + if ( ( value === '' ) && default_val ) + { + return default_val; + } + + // if the value is simply a string, return it + if ( /^[a-z_ -]+$/i.test( value ) ) + { + return value; + } + + // split on multiple limits (this will work fine if there's only + // one) + var data = ( ''+( value ) ).split( '/' ), + i = data.length; + + // simple comma addition (for thousands); XXX: this mess is an + // abomination + while ( i-- ) + { + data[ i ] = data[ i ] + .replace( /^.*(?:[0-9]{3}|[1-9]0{2})$/, function( number ) + { + var len = number.length, + ret = ''; + + // insert thousands separators into their proper + // positions + for ( var i = 0; i < len; i++ ) + { + ret += number.substr( i, 1 ); + + if ( ( ( len - i ) % 3 ) === 1 ) + { + ret += ','; + } + } + + // could handle this in the above loop, but this is more + // clear + return ret.replace( /,$/, '' ); + } ) + .replace( /^,?([0-9]{2,})$/, '$1,000' ) + .replace( /^,/, '' ); + } + + // re-join multiple limits + return data.join( '/' ); + }, + + 'multilimit': function( values, _, default_val ) + { + var limit = this._getAnswerStyler( 'limit' ); + + // if we're not an array, fall back to the normal limit styler + if ( typeof values !== 'object' ) + { + return limit.apply( this, arguments ); + } + + var formatted = [], + same = true; + + for ( var i in values ) + { + formatted.push( + limit.call( this, values[ i ], _, default_val ) + ); + + if ( i > 0 ) + { + same = ( same && formatted[ i ] === formatted[ i - 1 ] ); + } + } + + return ( same ) + ? formatted[ 0 ] + : formatted.join( '; ' ); + }, + + 'multitext': function( values, _, default_val ) + { + // for now + this._getAnswerStyler( 'multilimit' ).apply( + this, arguments + ); + }, + + 'deductible': function( value, _, default_val ) + { + // if no value was given, be sure that we use the proper default + // value + if ( ( value === '' ) && default_val ) + { + return default_val; + } + + return value + ' Deductible'; + }, + + /* + * display as accepted, rejected or default if available + */ + 'acceptReject': function( value, _, default_val ) + { + // use the default if no value + if ( ( value === '' ) && default_val ) + { + return default_val; + } + + var ret = value; + + if ( +value === 0 ) + { + ret = 'Rejected'; + } + else if ( +value === 1 ) + { + ret = 'Accepted'; + } + + return ret; + }, + + + 'includeExclude': function( value, _, default_val ) + { + // use the default if no value + if ( ( value === '' ) && default_val ) + { + return default_val; + } + + return ( +value === 0 ) + ? 'Excluded' + : 'Included'; + }, + + + /* + * display as a limit, rejected or default if available + */ + 'limitReject': function( value, _, default_val ) + { + // use the default if no value + if ( ( value === '' ) && default_val ) + { + return default_val; + } + + var limit = this._getAnswerStyler( 'number' ); + + if ( +value === 0 ) + { + ret = 'Rejected'; + } + else if ( +value === 1 ) + { + ret = 'Accepted'; + } + else + { + ret = limit.apply( this, arguments ); + } + + return ret; + }, + + + /** + * format thousands + */ + 'number': function( value ) + { + var str = value.toString().split( '.' ); + var len = str[0].length, + ret = ''; + + for ( var i = 0; i < len; i++ ) + { + ret += str[0].charAt( i ); + + if ( ( ( len - i ) % 3 ) === 1 ) + { + ret += ','; + } + } + + str[0] = ret.replace( /,$/, '' ); + + return str.join( '.' ) + }, + + 'state': function( value ) + { + return State.getName( value ); + }, + + /** + * Styles a no-yes answer + * + * A non-zero value is considered to be "Yes". An empty string (unless a + * default value is given) and "0" are considered to be "No". Default + * value is only returned if an empty string is provided. + * + * @param {string} value value to style + * @param {*} _ ignored + * @param {string_ default_val default value + * + * @return {string} styled answer + */ + 'noyes': function( value, _, default_val ) + { + // if a default value is provided, we will interpret an empty string + // as no value and return the default + if ( ( value === '' ) && default_val ) + { + return default_val; + } + + return ( value && ( value !== '0' ) ) + ? 'Yes' + : 'No'; + }, + + 'select': function( value, ref_id ) + { + var val = this._selectData[ref_id][value]; + + // return the string associated with the given value + // (the text for the option), or the given value if it does not + // exist + return ( val === undefined ) + ? value + : val; + }, + + 'manualDate': function( value ) + { + if( value.replace ) + { + return value.replace( + /^([0-9]{4})-([0-9]+)-([0-9]+)$/, + '$2/$3/$1' + ); + } + + return null; + }, + + 'date': function( value ) + { + var data = value.split( '-' ); + + // m/d/y + return data[1] + '/' + data[2] + '/' + data[0]; + }, + + 'dateTime': function( value ) + { + var ret_val = new Date( ( +value ) * 1000 ); + + // do not attempt to format if invalid date + if ( isNaN( ret_val.getDate() ) || value === '' ) + { + return ''; + } + + return ( ret_val.getMonth() + 1 ) + '/' + + ret_val.getDate() + '/' + + ret_val.getFullYear(); + }, + + percent: function( value ) + { + return value + '%'; + }, + + waitable: function( value ) + { + return value.replace( + /Please wait.../, + '
$&
' + ); + } + }, + + + __construct: function() + { + this._$context = jQuery; + }, + + + /** + * Returns the function to be used for the widget jQuery selector + * + * @return Function selector function + */ + getWidgetSelector: function() + { + return function( node, i, data ) + { + // name of the widget we're searching for + var name = data[3], + $node = $( node ); + + // if it's not a widget, bail + if ( !( $node.hasClass( 'widget' ) ) ) + { + return false; + } + + // if no name was given, then don't check for it (they just want to + // know if this is a widget) + if ( !( name ) ) + { + return true; + } + + // quick name check on self + if ( $node.attr( 'name' ) === ( name + '[]' ) ) + { + return true; + } + + // attempt to locate the element with the name we're looking for + var $named = $node.find( '[name="' + name + '[]"]' ); + if ( $named.length === 0 ) + { + return false; + } + + return true; + } + }, + + + getWidgetIdSelector: function() + { + return function( node, i, data ) + { + // name of the widget we're searching for + var id = data[3], + $node = $( node ); + + if ( !( $node.hasClass( 'widget' ) ) ) + { + return false; + } + + // if no name was given, then don't check for it (they just want to + // know if this is a widget) + if ( !( name ) ) + { + return true; + } + + // quick name check on self + if ( $node.attr( 'id' ) === id ) + { + return true; + } + + // attempt to locate the element with the name we're looking for + var $named = $node.find( '[id="' + id + '"]' ); + if ( $named.length === 0 ) + { + return false; + } + + return true; + } + }, + + + /** + * Applies the style to all DOM elements that are descendants of $content + * + * This method operates off of a very basic concept. It takes an array of + * data containing a jQuery selector and applies the associated attributes + * to the elements. These attributes are non-standard - that is, they are + * not valid HTML attributes. Dojo then parses out these attributes and + * generates the dijit HTML, replacing the existing element. + * + * @param jQuery $content parent element containing elements to style + * + * @return ElementStyler self to support method chaining + */ + apply: function( $content, parse ) + { + parse = ( parse === undefined ) ? true : !!parse; + + // if we're internal, show internal questions + if ( this._showInternal ) + { + $content.find( '.hidden.i' ).removeClass( 'hidden' ); + } + + return this; + }, + + + /** + * Called after the content is appended to the DOM for the first time + * + * This is used to do final processing for display. In this case, performing + * the actual styling based off of the previously set attributes. + * + * @return ElementStyler self to allow for method chaining + */ + postAppend: function( $content ) + { + return this; + }, + + + /** + * Retrieves the id associated with the given element + * + * The problem is that Dijits do not always use a single element. They'll + * often have multiple elements in order to achieve a certain effect. The id + * may be on a different element than the one that contains the correct name + * attribute. + * + * This method will attempt to find the id by checking first the given + * element, then its siblings, followed finally by its children. Having to + * check for siblings is slower than accessing directly, and having to go so + * far as to check the children is the slowest. It is uncommon to have to + * check the children, so that check is performed last. + * + * @param jQuery $element element to get id of + * + * @return String element id or undefined + */ + getIdFromElement: function( $element ) + { + // check to see if the given element has the id we're looking for + // first, otherwise the siblings/children most likely contain the id + // we're looking for + return $element.attr( 'widgetid' ) + || $element.attr( 'id' ) + || $element.siblings().filter( '[id]' ).attr( 'id' ) + || $element.children().filter( '[id]' ).attr( 'id' ); + }, + + + /** + * Gets the name from a given element + * + * This method is needed because the name attribute may exist on a different + * element than the one provided. + * + * @return String|undefined + */ + getNameFromElement: function( $element ) + { + if ( !( $element instanceof jQuery ) ) + { + // assume it's an id + $element = $( '#' + $element ); + } + + return $element.attr( 'name' ) + || $element.siblings().filter( '[name]' ).attr( 'name' ) + || $element.children().filter( '[name]' ).attr( 'name' ) + || ''; + }, + + + /** + * Returns the element with the name attribute + * + * This allows referencing the element that should be posted + * + * @return jQuery named element + */ + getNameElement: function( $element ) + { + // if the passed element has a name attribute, then no searching is + // needed + if ( $element.attr( 'name' ) ) + { + return $element; + } + + // attempt to find from siblings and children + return $element.siblings().filter( '[name]' ) + || $element.children().filter( '[name]' ) + }, + + + 'public setOptions': function( name, index, options, val, $context ) + { + var $element = this.getElementByName( name, index, null, $context ); + + // if the provided question is not a select, then we cannot add options + // to it---use the first index instead + if ( this._getElementType( name ) !== 'select' ) + { + $element.val( ( options[ 0 ] || { value: '' } ).value ); + return; + } + + // store the old value + val = val || $element.val(); + + $element.html(''); + + var answer_data = this._selectData[ name ] = {}; + for ( var item in options ) + { + var opt = options[ item ], + opt_value = opt.value === undefined || opt.value === null ? '' : opt.value; + + answer_data[ opt_value ] = opt.label; + + $element.append( + $( '