
/*

  SmartClient Ajax RIA system
  Version v9.0p_2021-05-01/LGPL Deployment (2021-05-01)

  Copyright 2000 and beyond Isomorphic Software, Inc. All rights reserved.
  "SmartClient" is a trademark of Isomorphic Software, Inc.

  LICENSE NOTICE
     INSTALLATION OR USE OF THIS SOFTWARE INDICATES YOUR ACCEPTANCE OF
     ISOMORPHIC SOFTWARE LICENSE TERMS. If you have received this file
     without an accompanying Isomorphic Software license file, please
     contact licensing@isomorphic.com for details. Unauthorized copying and
     use of this software is a violation of international copyright law.

  DEVELOPMENT ONLY - DO NOT DEPLOY
     This software is provided for evaluation, training, and development
     purposes only. It may include supplementary components that are not
     licensed for deployment. The separate DEPLOY package for this release
     contains SmartClient components that are licensed for deployment.

  PROPRIETARY & PROTECTED MATERIAL
     This software contains proprietary materials that are protected by
     contract and intellectual property law. You are expressly prohibited
     from attempting to reverse engineer this software or modify this
     software for human readability.

  CONTACT ISOMORPHIC
     For more information regarding license rights and restrictions, or to
     report possible license violations, please contact Isomorphic Software
     by email (licensing@isomorphic.com) or web (www.isomorphic.com).

*/
//> @class SpinnerItem
//
// Item for picking a number. Includes arrow buttons to increase / decrease the value
//
// @treeLocation Client Reference/Forms/Form Items
// @visibility external
// @example spinnerItem
//<
isc.ClassFactory.defineClass("SpinnerItem", "TextItem");

isc.SpinnerItem.addClassProperties({

    //> @const  SpinnerItem.INCREASE_ICON    (FormItemIcon Properties : {...} : IRW)
    // Icon to increase the spinner's value (an up-arrow by default)
    // @visibility external
    //<
    INCREASE_ICON:  {width:16, height:9, src:"[SKIN]/DynamicForm/Spinner_increase_icon.png",
                        name:"increase",
                        showOver:false,
                        showFocusedWithItem:false,
                        // We don't need to support native focus, and we'll use mouseStillDown
                        // rather than standard icon click to handle activation
                        imgOnly:true,
                        hspace:0
                    },

    //> @const  SpinnerItem.DECREASE_ICON   (FormItemIconProperties : {...} : IRW)
    // Icon to decrease the spinner's value (a down-arrow by default)
    // @visibility external
    //<
    DECREASE_ICON:  {width:16, height:9, src:"[SKIN]/DynamicForm/Spinner_decrease_icon.png",
                        name:"decrease",
                        showOver:false,
                        showFocusedWithItem:false,
                        imgOnly:true,
                        hspace:0
                    }

    //> @attr   spinnerItem.mask  (string : null : IRWA)
    // Not applicable to a SpinnerItem.
    // @visibility  external
    //<    
    //> @attr   spinnerItem.maskSaveLiterals   (boolean : null : IRWA)
    // Not applicable to a SpinnerItem.
    // @visibility  external
    //<    
    //> @attr   spinnerItem.maskPadChar   (string : " " : IRWA)
    // Not applicable to a SpinnerItem.
    // @visibility  external
    //<    
    //> @attr   spinnerItem.maskPromptChar   (string : "_" : IRWA)
    // Not applicable to a SpinnerItem.
    // @visibility  external
    //<    
    //> @attr   spinnerItem.maskOverwriteMode   (boolean : null : IRWA)
    // Not applicable to a SpinnerItem.
    // @visibility  external
    //<    
    
});

isc.SpinnerItem.addProperties({

    // Don't fire the change handler on every keypress, as we need the user to be able
    // to enter partial numbers (like the string "-") without us trying to validate it as a 
    // number
    changeOnKeypress:false,
    
    // Don't allow tabbing to the spinner icons - you already have keyboard support via up and
    // down arrows, and we don't support rolling the vals up and down via keypresses on the
    // icons...
    canTabToIcons:false,
    
    // Default to matching the height of the 2 spinners exactly.
    height:18,

    //> @attr   SpinnerItem.step    (number : 1 : IRW)
    //  How much should the value be incremented / decremented when the user hits the icons
    //  to increase / decrease the value?
    // @visibility external
    // @example spinnerItem
    //<
    step:1
    
    //>@attr    SpinnerItem.max (number : null : IRW)
    // Maximum valid value for this item    
    // @visibility external
    // @example spinnerItem
    //<
    
    //>@attr    SpinnerItem.min (number : null : IRW)
    // Minimum valid value for this item    
    // @visibility external
    // @example spinnerItem
    //<


});

isc.SpinnerItem.addMethods({
    
    // Override init to ensure 'step' is a valid numeric value
    init : function () {
        this.Super("init",arguments);
        var step = this.step;
        if (step != null && !isc.isA.Number(step)) {
            step = parseFloat(step);
            if (!isc.isA.Number(step)) step = 1;
            this.step = step;
        }
    },

    // Override 'setUpIcons' to add the Increase / Decrease icons
    _setUpIcons : function () {
        // Add the pickbutton to the set of icons
        if (this.icons == null) this.icons = [];
        
        var inc = isc.addProperties({}, isc.SpinnerItem.INCREASE_ICON),
            dec = isc.addProperties({}, isc.SpinnerItem.DECREASE_ICON);
            
        this.icons.addListAt([inc,dec], 0);
        
        this.Super("_setUpIcons", arguments);
    },

    // Override getIconsHTML to write the increase / decrease icons out, one above the other
    

//icon, over,disabled,focused
    _$iconsHTMLCellStart: "<td tabIndex='-1'" + (isc.Browser.isIE ? " style='font-size:0px'" : ""),
    _$rowspanEquals2GT: " rowspan='2'>",
    _$iconsHTMLCellEnd: "</td>",
    getIconsHTML : function () {
        if (!this.showIcons) return isc.emptyString;

        var template = this._spinnerTableTemplate;
        if (template == null) {
            
            
            var cellStart = this._$iconsHTMLCellStart,
                
                sampleIcon = {},
                vAlign = this._getIconVAlign(sampleIcon),
                vMargin = this._getIconVMargin(sampleIcon);

            template = this._spinnerTableTemplate = [
                "<table role='presentation' style='margin-top:",      // [0]
                vMargin,                                              // [1]
                ";margin-bottom:",                                    // [2]
                vMargin,                                              // [3]
                "' border=0 cellpadding=0 cellspacing=0><tbody><tr>", // [4]
                cellStart,                                            // [5]
                ">",                                                  // [6]
                null,                                                 // [7] this.getIconHTML(this.icons[0])
                "</td>",                                              // [8]
                                                                      // <- extra iconHTML inserted here
                                                                      // For each extra icon, there are 4 template entries
                                                                         // this._$iconsHTMLCellStart, // [0]
                                                                         // " rowspan='2'>",           // [1]
                                                                         // iconHTML,                  // [2]
                                                                         // "</td>"                    // [3]
                "</tr><tr>",                                          // [9 + 4 * nRoomForExtraIcons]
                cellStart,                                            // [10 + 4 * nRoomForExtraIcons]
                ">",                                                  // [11 + 4 * nRoomForExtraIcons]
                null,                                                 // [12 + 4 * nRoomForExtraIcons] this.getIconHTML(this.icons[1])
                "</td></tr></tbody></table>"                          // [13 + 4 * nRoomForExtraIcons]
            ];
            this._nRoomForExtraIcons = 0;
        }

        // How many extra icons do we have? (The first two are the increase and decrease spinner icons.)
        var nExtraNeedRoomFor = this.icons.length - 2;

        // If we need space for fewer icons, shrink the template by 4 times the difference between
        // the number of extra icons that the template can currently accommodate and the number
        // of extra icons that we have.
        if (nExtraNeedRoomFor < this._nRoomForExtraIcons) {
            template.splice(9 + 4 * nExtraNeedRoomFor, 4 * (this._nRoomForExtraIcons - nExtraNeedRoomFor));
            this._nRoomForExtraIcons = nExtraNeedRoomFor;

        // If we don't have enough space for extra icons, then insert 4 times the number of extra
        // template entries that we need.
        } else {
            var d = nExtraNeedRoomFor - this._nRoomForExtraIcons;
            if (d > 0) {
                var spliceArgs = [9, 0];
                spliceArgs[2 + 4 * d - 1] = null;
                template.splice.apply(template, spliceArgs);
                this._nRoomForExtraIcons = nExtraNeedRoomFor;
            }
        }

        template[7] = this.getIconHTML(this.icons[0]);
        template[12 + 4 * this._nRoomForExtraIcons] = this.getIconHTML(this.icons[1]);

        var i = 9;
        for (var d = 2; d < this.icons.length; ++d, i += 4) {
            var icon = this.icons[d];

            // don't write out the icon if it specified a showIf, which returns false
            if (!this._shouldShowIcon(icon) || this._writeIconIntoItem(icon)) {
                template[i] = null;
                template[i + 1] = null;
                template[i + 2] = null;
                template[i + 3] = null;
            } else {
                template[i] = this._$iconsHTMLCellStart;
                template[i + 1] = this._$rowspanEquals2GT;
                template[i + 2] = this.getIconHTML(icon);
                template[i + 3] = this._$iconsHTMLCellEnd;
            }
        }

        return template.join(isc.emptyString);
    },

    getIconHTML : function (icon) {
        if (this._isPrinting()) return null;
        return this.Super("getIconHTML", arguments);
    },

    getTextBoxStyle : function () {
        if (this._isPrinting()) return isc.TextItem.getInstanceProperty("textBoxStyle");
        else return this.Super("getTextBoxStyle", arguments);
    },

    // Override getIconVMargin to return zero for the spinners.
    _getIconVMargin : function(icon) {
        if (icon == this.icons[0] || icon == this.icons[1]) return 0;
        return this.Super("_getIconVMargin", arguments);
    },

    // we're writing our 2 spinner icons one above the other
    // Only account for the width of one of them, not both
    getTotalIconsWidth : function () {
        var width = this.Super("getTotalIconsWidth", arguments);
        // A width of zero implies we're not showing any icons
        if (width > 0) {
            var spinWidthExcess = Math.max(this.icons[0].width, this.icons[1].width);
            width -= spinWidthExcess;
        }
        return width;
    },

    // Use 'mouseStillDown' to handle the user holding the mouse over the increase/decrease 
    // icons
    mouseStillDown : function (form, item, event) {
        if (this.isDisabled() || this.isReadOnly()) return;
        
        // increment counter for simple value ramp
        this._mouseStillDownCounter++;
        
        if (this._valueIsDirty) this.updateValue();
        
        var nativeTarget = event.nativeTarget;

        var lastTargetIcon = this._lastTargetIcon,
            targetIcon = null;
        if (nativeTarget == this._getIconImgElement(this.icons[0])) {
            this.increaseValue();
            targetIcon = this.icons[0];
        } else if (nativeTarget == this._getIconImgElement(this.icons[1])) {
            this.decreaseValue();
            targetIcon = this.icons[1];
        }

        if (lastTargetIcon != targetIcon) {
            if (lastTargetIcon != null) {
                var img = this._getIconImgElement(lastTargetIcon);
                if (img) {
                    this._iconBlur(lastTargetIcon.name, img);
                }
            }

            this._lastTargetIcon = targetIcon;
            if (targetIcon != null) {
                var img = this._getIconImgElement(targetIcon);
                if (img) {
                    this._iconFocus(targetIcon.name, img);
                }
            }
        }
    },
    
    // reset counter for simple value ramp
    mouseDown : function (form, item, event) {
        if (this.isDisabled() || this.isReadOnly()) return;
        // force focus into the item on mouseDown - required to ensure we save out change made via
        // icon click
        if (!this.hasFocus) this.focusInItem();
        this._mouseStillDownCounter = 0;
        isc.Page.setEvent(isc.EH.MOUSE_UP, this, isc.Page.FIRE_ONCE, "_clearLastTargetIcon");
    },

    _clearLastTargetIcon : function () {
        var lastTargetIcon = this._lastTargetIcon;
        if (lastTargetIcon != null) {
            var img = this._getIconImgElement(lastTargetIcon);
            if (img) {
                this._iconBlur(lastTargetIcon.name, img);
            }
        }
        this._lastTargetIcon = null;
    },

    // Override handleKeyPress to increase / decrease on up / down arrow keys
    handleKeyPress : function (event, eventInfo) {

        if (this.Super("handleKeyPress", arguments) == false) return false;
        var keyName = event.keyName,
            readOnly = this.isReadOnly()
        ;
        if (!readOnly && keyName == "Arrow_Up") {
            this.increaseValue();
            return false;
        }
        if (!readOnly && keyName == "Arrow_Down") {
            this.decreaseValue();
            return false;
        }
    },
    
    increaseValue : function () {
        this.updateValue();
        var value = this.getValue();
        if (value != null && this.max == value) return;
        var mouseStillDownCounter = this._mouseStillDownCounter;
        // If we get called directly when the mouse is not down, don't crash, just increment by
        // a single step.
        // Otherwise apply a simple value ramp - double the step size every 2 seconds
        var step = this.step * (mouseStillDownCounter != null ? 
                            Math.pow(2, 
                                Math.floor(
                                    this._mouseStillDownCounter/(2000/isc.EH.STILL_DOWN_DELAY)
                                )
                            ) :
                                1);
        return this._increaseValue(value, step);
    },
    
    decreaseValue : function () {
        this.updateValue();    
        var value = this.getValue();
        if (value != null && this.min == value) return;
        var mouseStillDownCounter = this._mouseStillDownCounter,
            step = (0-this.step) * (mouseStillDownCounter != null ? 
                            Math.pow(2, 
                                Math.floor(
                                    this._mouseStillDownCounter/(2000/isc.EH.STILL_DOWN_DELAY)
                                )
                            ) :
                                1);
        
        return this._increaseValue(value, step);
    },
    
    // Actually modify the value    
    
    _increaseValue : function (value, step) {

        var minVal = this.min, maxVal = this.max;

        // If it's not a value to start with, default to zero.
        // If zero is outside our range, default to minVal (or maxVal if min is not defined)
        // instead
        if (!isc.isA.Number(value)) {
            value = 0;
            if ((minVal != null && value < minVal) || (maxVal != null && value > maxVal)) {
                
                value = (minVal != null ? minVal : maxVal);
            }
        // Otherwise increment as requested
        } else {
            
            
            var stepDP,
                valueDP;
            
            if (Math.round(step) == step) {
                stepDP = 0
            } else {
                
                var stepString = step + "";
                stepDP = stepString.length - (stepString.indexOf(".") +1);
            }
            if (Math.round(value) == value) {
                valueDP = 0;
            } else {
                var valueString = value + "";
                valueDP = valueString.length - (valueString.indexOf(".") +1);
            }
            value += step;
            
            var decimalPlaces = Math.max(stepDP, valueDP);
            if (decimalPlaces > 0) {
                
                value = parseFloat(value.toFixed(decimalPlaces));
            }
            
            // Don't go beyond max / min
            if (step > 0 && maxVal != null && value > maxVal) value = maxVal;
            else if (step < 0 && minVal != null && value < minVal) value = minVal;
        }
        var form = this.form,
            record = this.form ? this.form.getValues() : null,
            formattedVal = (this.formatEditorValue != null) 
                            ? this.formatEditorValue(value,record,form,this)
                            : value;
                            
        this.setElementValue(formattedVal);
        this.updateValue();    

    },
    
    // Override mapDisplayToValue to return a numeric value rather than a string.
    mapDisplayToValue : function (value) {
        value = this.Super("mapDisplayToValue", arguments);
        
        if (isc.isA.String(value)) {
            var floatVal = parseFloat(value);
            if (floatVal == value) value = floatVal;
        }
        return value;
    },
    
    // Override updateValue (called when a user modifies the value) to validate the value as 
    // numeric.
    updateValue : function () {

        var value = this.getElementValue();

        // unmap the value if necessary 
        value = this.mapDisplayToValue(value);

        if (value == this._value) return;
        
        // If the user entered an invalid number just refuse to accept it.
        if (value != null && (!isc.isA.Number(value) || (this.max != null && value > this.max) || 
                                (this.min != null && value < this.min)) )
        {
            var oldValue = this.mapValueToDisplay(this._value);
            this.setElementValue(oldValue);
            return;
        }

        // Allow the superclass implementation to actually update the value
        this.Super("updateValue", arguments); 
    },
    
    // Override setValue to disallow setting to a non numeric value programatically
    setValue : function (value, allowNullValue, a,b,c,d) {
        if (value != null && !isc.isA.Number(value)) {
            var numVal = parseFloat(value);
            if (numVal == value) value = numVal;
            else {
                this.logWarn("setValue(): passed '" + value + 
                                "'. This is not a valid number - rejecting this value");
                value = null;
            }
        }
        if (value != null) {
            if (this.max != null && value > this.max) {
                this.logWarn("setValue passed "+ value + 
                                " - exceeds specified maximum. Clamping to this.max.");
                value = this.max;
            }
            if (this.min != null && value < this.min) {
                this.logWarn("setValue passed "+ value + 
                                " - less than specified minimum. Clamping to this.min.");
                value = this.min;
            }
        }
        
        return this.invokeSuper(isc.SpinnerItem, "setValue", value, allowNullValue, a,b,c,d);
    }
	    
});
