You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flex.apache.org by ca...@apache.org on 2014/01/22 00:50:29 UTC
git commit: [flex-sdk] [refs/heads/develop] - FLEX-34053 - New
MaskedTextinput in experimental library
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 <ca...@apache.org>
Authored: Wed Jan 22 00:50:00 2014 +0100
Committer: Carlos Rovira <ca...@apache.org>
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;
+ }
+ }
+}