Updated Branches:
  refs/heads/develop 61dd472ca -> 61aa3601c

FLEX-34053 - New MaskedTextinput in experimental library


Project: http://git-wip-us.apache.org/repos/asf/flex-sdk/repo
Commit: http://git-wip-us.apache.org/repos/asf/flex-sdk/commit/61aa3601
Tree: http://git-wip-us.apache.org/repos/asf/flex-sdk/tree/61aa3601
Diff: http://git-wip-us.apache.org/repos/asf/flex-sdk/diff/61aa3601

Branch: refs/heads/develop
Commit: 61aa3601c8c57f2855e18e11fa16f4180de42e05
Parents: 61dd472
Author: Carlos Rovira <[email protected]>
Authored: Wed Jan 22 00:50:00 2014 +0100
Committer: Carlos Rovira <[email protected]>
Committed: Wed Jan 22 00:50:00 2014 +0100

----------------------------------------------------------------------
 frameworks/projects/experimental/defaults.css   |  19 +
 frameworks/projects/experimental/manifest.xml   |   2 +
 .../src/spark/components/MaskedTextInput.as     | 598 +++++++++++++++++++
 3 files changed, 619 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/flex-sdk/blob/61aa3601/frameworks/projects/experimental/defaults.css
----------------------------------------------------------------------
diff --git a/frameworks/projects/experimental/defaults.css 
b/frameworks/projects/experimental/defaults.css
index eb5e83b..033d0b0 100644
--- a/frameworks/projects/experimental/defaults.css
+++ b/frameworks/projects/experimental/defaults.css
@@ -81,4 +81,23 @@ Menu {
 
 MenuBar {
     skinClass: ClassReference("spark.skins.MenuBarSkin");
+}
+
+MaskedTextInput {
+    fontFamily: _typewriter;
+}
+
+MaskedTextInput #textDisplay {
+    color: #000000;
+    fontStyle: normal;
+    typographicCase: uppercase;
+}
+
+MaskedTextInput #promptDisplay {
+    color: #cccccc;
+    fontStyle: normal;
+}
+
+MaskedTextInput:disabledWithPrompt {
+    color: #cccccc;
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/flex-sdk/blob/61aa3601/frameworks/projects/experimental/manifest.xml
----------------------------------------------------------------------
diff --git a/frameworks/projects/experimental/manifest.xml 
b/frameworks/projects/experimental/manifest.xml
index 2aaf469..440eae5 100644
--- a/frameworks/projects/experimental/manifest.xml
+++ b/frameworks/projects/experimental/manifest.xml
@@ -39,6 +39,8 @@
     <component id="InlineScroller" class="spark.components.InlineScroller"/>
     <!-- ColorPicker -->
     <component id="ColorPicker" class="spark.components.ColorPicker"/>
+    <!-- MaskedTextInput -->
+    <component id="MaskedTextInput" class="spark.components.MaskedTextInput"/>
     <!-- Menu -->
     <component id="Menu" class="spark.components.Menu"/>
     <component id="MenuItemRenderer" 
class="spark.components.itemRenderers.MenuItemRenderer"/>

http://git-wip-us.apache.org/repos/asf/flex-sdk/blob/61aa3601/frameworks/projects/experimental/src/spark/components/MaskedTextInput.as
----------------------------------------------------------------------
diff --git 
a/frameworks/projects/experimental/src/spark/components/MaskedTextInput.as 
b/frameworks/projects/experimental/src/spark/components/MaskedTextInput.as
new file mode 100644
index 0000000..f8c0bb2
--- /dev/null
+++ b/frameworks/projects/experimental/src/spark/components/MaskedTextInput.as
@@ -0,0 +1,598 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+//  Licensed to the Apache Software Foundation (ASF) under one or more
+//  contributor license agreements.  See the NOTICE file distributed with
+//  this work for additional information regarding copyright ownership.
+//  The ASF licenses this file to You under the Apache License, Version 2.0
+//  (the "License"); you may not use this file except in compliance with
+//  the License.  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+package spark.components {
+    import flash.events.Event;
+    import flash.events.TextEvent;
+
+    import flashx.textLayout.edit.SelectionState;
+    import flashx.textLayout.operations.CompositeOperation;
+    import flashx.textLayout.operations.DeleteTextOperation;
+    import flashx.textLayout.operations.InsertTextOperation;
+
+    import mx.core.mx_internal;
+    import mx.utils.StringUtil;
+
+    import spark.components.RichEditableText;
+    import spark.components.TextInput;
+    import spark.events.TextOperationEvent;
+
+    use namespace mx_internal;
+
+    /**
+     * Masked Text Input Component
+     *
+     * A TextInput extension with a mask text and a string pattern of 
separators that constraints the text
+     * introduced by the user to the available characters dictated by the 
pattern and inserts or deletes
+     * separator characters guided by the mask text pattern.
+     *
+     * This control is widely used in applications to insert different kind of 
data like dates,
+     * bank accounts, plates or phone numbers...
+     *
+     * Template rules:
+     *
+     *      # - numeric-only,
+     *      @ - alphabetic-only,
+     *      ? - any
+     *
+     * configuration properties:
+     *
+     *      maskText            : The mask base string for the component logic
+     *                            defaults to ""
+     *      separators          : The characters specified to act as 
separators that must be present in the maskText
+     *                            defaults to "-+/|()[]{}."
+     *      textMaskPrompt      : User defined prompt to override default 
behaviour.
+     *                            defaults to ""
+     *      placeHolder         : A character to show instead internal mask 
template characters (#, @ and ?)
+     *                            defaults to "_"
+     *      usePlaceHolder      : If true, show placeHolder instead #, @ or ?, 
if false show real characters
+     *                            defaults to true
+     *      hideSeparatorInText : If true, separator is hidden in text but 
shown in mask, viceversa if false.
+     *                            defaults to true
+     *      showMaskWhileWrite  : If true the remaining mask is shown while 
user write as a helper. false
+     *                            hide mask as soon as the user interact with 
the control
+     *                            defaults to true
+     *
+     * Some sample mask patterns:
+     *
+     * Date:  ##/##/####
+     * Phone: (###)###.##.##.##
+     * IBAN:  ES##-####-####-##-##########
+     * CCC:   ####-####-##-##########
+     *
+     * @author Carlos Rovira (http://www.carlosrovira.com)
+     * @created 05/12/13
+     * @updated 22/01/14
+     */
+    public class MaskedTextInput extends TextInput {
+        public function MaskedTextInput() {
+            super();
+
+            addEventListener(TextOperationEvent.CHANGING, verifyInsertedText);
+            addEventListener(TextOperationEvent.CHANGE, formatAsYouType);
+            addEventListener(TextEvent.TEXT_INPUT, overrideText);
+        }
+
+        //----------------------------------
+        //  PUBLIC
+        //----------------------------------
+
+        //----------------------------------
+        //  text
+        //----------------------------------
+        private var textChanged:Boolean = false;
+
+        /**
+         * get text formated with separators
+         */
+        public function get fullText():String {
+            return super.text;
+        }
+
+        /**
+         * get the raw text removing separators
+         */
+        override public function get text():String {
+            return cleanText(fullText);
+        }
+
+        /**
+         * remove not allowed separators
+         * @param value
+         * @return
+         */
+        private function cleanText(value:String):String {
+            var rawText:String = "";
+            for (var index:int = 0; index < value.length; index++) {
+                var aChar:String = value.charAt(index);
+                for (var sepIndex:int = 0; sepIndex < separators.length; 
sepIndex++) {
+                    var sepChar:String = separators.charAt(sepIndex);
+                    var found:Boolean = false;
+                    if (aChar == sepChar) {
+                        found = true;
+                        break;
+                    }
+                }
+                if (!found) {
+                    rawText += aChar;
+                }
+            }
+            return rawText;
+        }
+
+
+        private var storean:int = 0;
+        private var storeac:int = 0;
+
+        /**
+         * program a format of the entered text based on mask text and 
separators
+         * @param value
+         */
+        [Bindable("change")]
+        [Bindable("textChanged")]
+        // Compiler will strip leading and trailing whitespace from text 
string.
+        [CollapseWhiteSpace]
+        override public function set text(value:String):void {
+            if (super.text !== value) {
+                storean = selectionAnchorPosition;
+                storeac = selectionActivePosition;
+                super.text = value;
+                textChanged = true;
+                invalidateProperties();
+            }
+        }
+
+        //----------------------------------
+        //  mask text
+        //----------------------------------
+        private var _maskText:String = "";
+        private var maskedTextChanged:Boolean = true;
+
+        [Bindable]
+        public function get maskText():String {
+            return _maskText;
+        }
+
+        public function set maskText(value:String):void {
+            if (maskText !== value) {
+                _maskText = value;
+                maskedTextChanged = true;
+                invalidateProperties();
+            }
+        }
+
+        //----------------------------------
+        //  separators
+        //----------------------------------
+        private var _separators:String = "- +/|()[]{}.";
+        private var separatorsChanged:Boolean = true;
+
+        [Bindable]
+        public function get separators():String {
+            return _separators;
+        }
+
+        public function set separators(value:String):void {
+            if (separators !== value) {
+                separatorsChanged = true;
+                _separators = value;
+                invalidateProperties();
+            }
+        }
+
+        /**
+         * user defined text prompt. For example for date mask ("##/##/####") 
you could use textMaskPrompt "dd/mm/yyyy"
+         */
+        public var textMaskPrompt:String = "";
+
+        /**
+         * the character to represent a input character location
+         */
+        [Bindable]
+        public var placeHolder:String = "_";
+
+        /**
+         * substitute the prompt with the selected place holder for all 
special characters (#,@ and ?)
+         */
+        [Bindable]
+        public var usePlaceHolder:Boolean = true;
+
+        /**
+         * use blank character instead of separator character
+         */
+        [Bindable]
+        public var hideSeparatorInText:Boolean = true;
+
+        /**
+         * show the mask text while user writes, removing characters as user 
types
+         */
+        private var _showMaskWhileWrite:Boolean = true;
+
+        [Bindable]
+        public function get showMaskWhileWrite():Boolean {
+            return _showMaskWhileWrite;
+        }
+
+        public function set showMaskWhileWrite(value:Boolean):void {
+            _showMaskWhileWrite = value;
+            invalidateSkinState();
+        }
+
+        //----------------------------------
+        //  PRIVATE
+        //----------------------------------
+
+        /**
+         * internal character to represent blank space in text
+         */
+        private static const BLANK_SEPARATOR:String = " ";
+
+        /**
+         * an internal array to maintain the insertion point of separators. 
This array is filled based on the
+         * mask text pattern and the separators available
+         */
+        private var separatorLocations:Array = null;
+
+        //----------------------------------
+        //  COMPONENT LIFE CYCLE
+        //----------------------------------
+
+        /**
+         * getCurrentSkinState
+         * @return the new state
+         */
+        override protected function getCurrentSkinState():String {
+            if (showMaskWhileWrite) {
+                if (enabled && skin && skin.hasState("normalWithPrompt"))
+                    return "normalWithPrompt";
+                if (!enabled && skin && skin.hasState("disabledWithPrompt"))
+                    return "disabledWithPrompt";
+            }
+
+            return super.getCurrentSkinState();
+        }
+
+        /**
+         * commit properties
+         */
+        override protected function commitProperties():void {
+            super.commitProperties();
+
+            if (maskedTextChanged) {
+                typicalText = maskText;
+                maxChars = maskText.length;
+            }
+
+            if (separatorsChanged || maskedTextChanged) {
+                // create the array of separator locations based on mask text 
and available separators
+                separatorLocations = [];
+                for (var maskIndex:int = 0; maskIndex < maskText.length; 
maskIndex++) {
+                    var maskChar:String = getMaskCharAt(maskIndex);
+                    for (var sepIndex:int = 0; sepIndex < separators.length; 
sepIndex++) {
+                        var sepChar:String = separators.charAt(sepIndex);
+                        if (maskChar == sepChar) {
+                            separatorLocations.push(maskIndex);
+                        }
+                    }
+                }
+            }
+
+            if (textChanged) {
+                selectAll();
+                insertText(formatTextWithMask(text));
+                selectRange(storean, storeac);
+            }
+
+            separatorsChanged = maskedTextChanged = textChanged = false;
+
+            updatePrompt();
+        }
+
+        //----------------------------------
+        //  PROTECTED
+        //----------------------------------
+
+        /**
+         * format programmatic text (not introduced by user) in textDisplay 
control with the mask
+         * (i.e.: assigned string to text property, trigger, binding, ...)
+         */
+        protected function formatTextWithMask(value:String):String {
+            var stack:Array = value.split("");
+            var outputText:String = "";
+
+            for (var i:int = 0; i < maskText.length; i++) {
+                if (stack.length == 0) {
+                    break;
+                }
+                if (isSeparator(i)) {//if is separator location add separator
+                    outputText += getMaskCharAt(i);
+                } else { // if not add the expected value char
+                    outputText += restrictToMaskPattern(stack.shift(), i);
+                }
+            }
+
+            return outputText;
+        }
+
+        /**
+         * verify insertion to avoid characters not allowed in mask
+         * @param event the TextOperationEvent event
+         */
+        protected function verifyInsertedText(event:TextOperationEvent):void {
+            // filter now allowed characters
+            var an:int = selectionAnchorPosition;
+            var insertTextOp:InsertTextOperation = null;
+            if (event.operation is InsertTextOperation && an != maxChars) {
+                insertTextOp = event.operation as InsertTextOperation;
+                if (restrictToMaskPattern(insertTextOp.text, an) == ""
+                        || (restrictToMaskPattern(insertTextOp.text, an + 1) 
== "" && isMaskSeparatorLocation(an))) {
+                    event.preventDefault();
+                }
+            }
+        }
+
+        /**
+         * add or remove separator character as we type in the text input.
+         * Note that override text when all characters are in place
+         * is not supported (see overrideText method)
+         * @param event the TextOperationEvent event
+         */
+        protected function formatAsYouType(event:TextOperationEvent):void {
+            var stack:Array = text.split("");
+            var outputText:String = "";
+            var an:int = selectionAnchorPosition;
+            var ac:int = selectionAnchorPosition;
+            var offset:int = 0;//caret advances one position by default (on 
insert and deletion)
+
+            //copy/paste
+            if (event.operation is CompositeOperation) {
+                var operations:Array = (event.operation as 
CompositeOperation).operations;
+
+                if (operations[1] is InsertTextOperation) {
+                    var copyedText:String = cleanText((operations[1] as 
InsertTextOperation).text);
+                    outputText = formatTextWithMask(copyedText);
+                    an = ac = outputText.length;
+                }
+            }
+            //insert
+            else if (event.operation is InsertTextOperation) {
+
+                if ((event.operation as 
InsertTextOperation).deleteSelectionState != null) {
+                    //OVERRIDING INSERT
+                    if (ac < maxChars && isSeparator(ac)) {
+                        outputText = super.text.substring(0, ac + 1) + 
(event.operation as InsertTextOperation).text + super.text.substring(ac + 2);
+                    } else {
+                        outputText = super.text.substring(0, ac) + 
(event.operation as InsertTextOperation).text + super.text.substring(ac + 1);
+                    }
+
+                    if (isSeparator(ac)) {
+                        an = an + 2;
+                        ac = ac + 2;
+                    }
+                    else {
+                        an = an + 1;
+                        ac = ac + 1;
+                    }
+                } else {
+
+                    for (var i:int = 0; i < maskText.length; i++) {
+                        if (stack.length == 0) {
+                            break;
+                        }
+                        if (isMaskSeparatorLocation(i)) {
+                            outputText += getMaskCharAt(i);
+                            offset += 1;
+                        } else {
+                            outputText += restrictToMaskPattern(stack.shift(), 
i);
+                        }
+                    }
+
+                    //on override caret does not advance
+                    if (super.text.length > an) {
+                        //override on separator
+                        if (isSeparator(an - 1)) {
+                            offset = getDeletePosition(an) + 1;
+                        }
+                        else if (isSeparator(an)) {
+                            offset = 1;
+                        }
+                        else {
+                            offset = 0;
+                        }
+                    }
+
+                    an = ac = selectionAnchorPosition + offset;
+                }
+            }
+            //delete
+            else if (event.operation is DeleteTextOperation) {
+                if (isSeparator(an - 1)) {
+                    offset = consecutiveSeparators(an - 1);
+                }
+                else if (isSeparator(an)) {
+                    stack.splice(an - getDeletePosition(an) - 1, 1);
+                    offset = 1;
+                }
+
+                for (i = 0; i < maskText.length; i++) {
+                    if (stack.length == 0) {
+                        break;
+                    }
+                    if (isMaskSeparatorLocation(i)) {
+                        outputText += getMaskCharAt(i);
+                    } else {
+                        outputText += restrictToMaskPattern(stack.shift(), i);
+                    }
+                }
+
+                an = ac = selectionActivePosition - offset;
+            }
+
+            selectAll();
+            insertText(outputText);
+            selectRange(an, ac);
+
+            updatePrompt();
+
+            dispatchEvent(new Event("textChanged"));
+        }
+
+        /**
+         * used when text exist and is as long as maxChars and cursor is not at
+         * the end of the text.
+         * overrides the actual text as we type (replacing text as we type)
+         * @param event the TextEvent
+         */
+        protected function overrideText(event:TextEvent):void {
+            var an:int = selectionAnchorPosition;
+            var ac:int = selectionActivePosition;
+
+            // text is full, overwrite characters
+            if (super.text.length == maxChars) {
+                // filter now allowed characters
+                if (restrictToMaskPattern(event.text, an) == ""
+                        || (restrictToMaskPattern(event.text, an + 1) == "" && 
isMaskSeparatorLocation(an))) {
+                    return;
+                }
+
+                var operationState:SelectionState = new 
SelectionState(RichEditableText(textDisplay).textFlow, an, ac + 1);
+                var operation:InsertTextOperation = new 
InsertTextOperation(operationState, event.text, operationState);
+                var changeEvent:TextOperationEvent = new 
TextOperationEvent(TextOperationEvent.CHANGE, false, true, operation);
+                dispatchEvent(changeEvent);
+            }
+        }
+
+        //----------------------------------
+        //  PRIVATE
+        //----------------------------------
+
+        /**
+         * update prompt
+         */
+        private function updatePrompt():void {
+            var _prompt:String = "";
+            var textLength:int = super.text.length;
+            for (var i:int = 0; i < textLength; i++) {
+                if (isSeparator(i)) {//if is separator location add separator
+                    _prompt += hideSeparatorInText ? getMaskCharAt(i) : 
BLANK_SEPARATOR;
+                } else { // if not add the expected value char
+                    _prompt += BLANK_SEPARATOR;
+                }
+            }
+            _prompt += textMaskPrompt == "" ? maskText.substring(textLength) : 
textMaskPrompt.substring(textLength);
+
+            if (usePlaceHolder && textMaskPrompt == "") {
+                _prompt = _prompt.replace(/#/g, placeHolder);
+                _prompt = _prompt.replace(/@/g, placeHolder);
+                _prompt = _prompt.replace(/?/g, placeHolder);
+            }
+
+            prompt = _prompt;
+        }
+
+        /**
+         * Manage template rules:
+         *      # - numeric-only,
+         *      @ - alphabetic-only,
+         *      ? - any
+         * @param inputChar
+         * @param position
+         * @return the restricted character or the mask character
+         */
+        private function restrictToMaskPattern(inputChar:String, 
position:int):String {
+            var maskChar:String = getMaskCharAt(position);
+            switch (maskChar) {
+                case "#":
+                    return StringUtil.restrict(inputChar, "0-9");
+                case "@":
+                    return StringUtil.restrict(inputChar, "a-zA-Z");
+                case "?":
+                    return inputChar;
+            }
+            return maskChar;
+        }
+
+
+        //----------------------------------
+        //  PRIVATE AUTOMATA'S LOGIC
+        //----------------------------------
+
+        /**
+         * get the mask char at index location
+         * @param index
+         * @return
+         */
+        private function getMaskCharAt(index:int):String {
+            return maskText.charAt(index);
+        }
+
+        /**
+         *
+         * @param index
+         * @return true if location in maskText is separator, false if is 
placeHolder location
+         */
+        private function isMaskSeparatorLocation(index:int):Boolean {
+            for (var sepIndex:int = 0; sepIndex < separators.length; 
sepIndex++) {
+                var sepChar:String = separators.charAt(sepIndex);
+                if (getMaskCharAt(index) == sepChar) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * @param index
+         * @return true if there is separator at index, false otherwise
+         */
+        private function isSeparator(index:int):Boolean {
+            for (var i:int = 0; i < separatorLocations.length; i++) {
+                if (index == separatorLocations[i]) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * @param index
+         * @return return value to rest from anchor to splice chars when 
remove, 0 otherwise.
+         */
+        private function getDeletePosition(index:int):int {
+            for (var i:int = 0; i < separatorLocations.length; i++) {
+                if (index == separatorLocations[i]) {
+                    return i;
+                }
+            }
+            return 0;
+        }
+
+        /**
+         * @param index
+         * @return return value to rest from anchor to splice chars when 
remove, 0 otherwise.
+         */
+        private function consecutiveSeparators(index:int):int {
+            if (isSeparator(index - 1)) {
+                return 1 + consecutiveSeparators(index - 1);
+            } else {
+                return 1;
+            }
+            return 0;
+        }
+    }
+}

Reply via email to