frameworks/projects/experimental/src/spark/components/MaskedTextInput.as (542 lines of code) (raw):

//////////////////////////////////////////////////////////////////////////////// // // 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.EditManager; import flashx.textLayout.edit.SelectionState; import flashx.textLayout.operations.CompositeOperation; import flashx.textLayout.operations.CopyOperation; import flashx.textLayout.operations.DeleteTextOperation; import flashx.textLayout.operations.InsertTextOperation; import flashx.textLayout.tlf_internal; import mx.utils.StringUtil; import spark.components.RichEditableText; import spark.components.TextInput; import spark.events.TextOperationEvent; use namespace tlf_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: ####-####-##-########## */ 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) { var insertOp:InsertTextOperation = event.operation as InsertTextOperation; if (insertOp.deleteSelectionState != null && !insertOp.deleteSelectionState.tlf_internal::selectionManagerOperationState) { //OVERRIDING INSERT if (EditManager.overwriteMode) { //windows insert mode on (note that Flash Player does not track insertion mode state before running a SWF) if (isSeparator(ac - 1)) { outputText = super.text.substring(0, insertOp.originalSelectionState.anchorPosition + 1) + super.text.substring(insertOp.originalSelectionState.anchorPosition + 2); } else { outputText = super.text; } an -= 1; ac -= 1; } else if (isSeparator(ac)) { outputText = super.text.substring(0, insertOp.originalSelectionState.anchorPosition + 1) + insertOp.text + super.text.substring(insertOp.originalSelectionState.activePosition + 1); } else { outputText = super.text.substring(0, insertOp.originalSelectionState.anchorPosition) + insertOp.text + super.text.substring(insertOp.originalSelectionState.activePosition); } outputText = formatTextWithMask(cleanText(outputText)); if (isSeparator(ac)) { an = an + 2; ac = ac + 2; } else { an = an + 1; ac = ac + 1; } } else { //INSERT (TEXT NOT COMPLETE) 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; } //copy else if (event.operation is CopyOperation) { return; // avoid to remove all text on copy operation } 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 { //windows insert mode on (note that Flash Player does not track insertion mode state before running a SWF) if (EditManager.overwriteMode) { return; } 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); } } /** * Manage template rules: * # - numeric-only, * @ - alphabetic-only, * ? - any * @param inputChar * @param position * @return the restricted character or the mask character */ protected 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; } /** * update prompt */ protected 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; } //---------------------------------- // 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; } } }