| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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.utils.Dictionary; |
| |
| import mx.collections.ArrayCollection; |
| import mx.collections.IList; |
| import mx.collections.ISort; |
| import mx.core.ClassFactory; |
| import mx.core.IFactory; |
| import mx.core.IVisualElementContainer; |
| import mx.core.mx_internal; |
| import mx.events.FlexEvent; |
| |
| import spark.collections.Sort; |
| import spark.collections.SortField; |
| import spark.components.calendarClasses.DateAndTimeProvider; |
| import spark.components.calendarClasses.DateSelectorDisplayMode; |
| import spark.components.calendarClasses.DateSpinnerItemRenderer; |
| import spark.components.calendarClasses.YearProvider; |
| import spark.components.supportClasses.SkinnableComponent; |
| import spark.events.IndexChangeEvent; |
| import spark.formatters.DateTimeFormatter; |
| import spark.formatters.NumberFormatter; |
| import spark.globalization.supportClasses.CalendarDate; |
| import spark.globalization.supportClasses.DateTimeFormatterEx; |
| |
| use namespace mx_internal; |
| |
| [Exclude(name="textAlign", kind="style")] |
| |
| //-------------------------------------- |
| // Events |
| //-------------------------------------- |
| /** |
| * Dispatched after the selected date has been changed by the user. |
| * |
| * @eventType flash.events.Event.CHANGE |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| [Event(name="change", type="flash.events.Event")] |
| |
| /** |
| * Dispatched after the selected date has been changed, either |
| * by the user (i.e. interactively) or programmatically. |
| * |
| * @eventType mx.events.FlexEvent.VALUE_COMMIT |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| [Event(name="valueCommit", type="mx.events.FlexEvent")] |
| |
| //-------------------------------------- |
| // Styles |
| //-------------------------------------- |
| |
| include "../styles/metadata/StyleableTextFieldTextStyles.as" |
| |
| /** |
| * The locale of the component. Controls how dates are formatted, e.g. in what order the fields |
| * are listed and what additional date-related characters are shown, if any. Uses standard locale |
| * identifiers as described in Unicode Technical Standard #35. For example "en", "en_US" and "en-US" |
| * are all English, "ja" is Japanese. If the specified locale is not supported on the platform, "en_US" |
| * will be used. To determine if a locale is supported, use |
| * <code>DateTimeFormatter.getAvailableLocaleIDNames()</code> |
| * |
| * <p>The default value is undefined. This property inherits its value from an ancestor; if still |
| * undefined, it inherits from the global <code>locale</code> style.</p> |
| * |
| * <p>When using the Spark formatters and globalization classes, you can set this style on the root |
| * application to the value of the <code>LocaleID.DEFAULT</code> constant. |
| * Those classes will then use the client operating system's international preferences.</p> |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| [Style(name="locale", type="String", inherit="yes")] |
| |
| /** |
| * Color applied for the date items that match today's date. |
| * For example, if this is set to "0x0000FF" and today's date is 1/1/2011, then the month |
| * "January", the date "1", and the year "2011" will be in blue text on the spinners. This color |
| * is not applied to time items. |
| * |
| * <p>The default value is inherited from the global styles; see the framework defaults.css for |
| * the specific value.</p> |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| [Style(name="accentColor", type="uint", format="Color", inherit="yes")] |
| |
| //-------------------------------------- |
| // Other metadata |
| //-------------------------------------- |
| |
| [IconFile("DateSpinner.png")] |
| |
| /** |
| * The DateSpinner control presents an interface for picking a particular date or time. |
| * |
| * <p>The DateSpinner control can display the date, the time, or the date and time, based on the |
| * value of the <code>displayMode</code> property.</p> |
| * |
| * <p>The following images shows the three modes of the DateSpinner control:</p> |
| * |
| * <p> |
| * <img src="../../images/datespinner_types.png" alt="DateSpinner types"/> |
| * </p> |
| * |
| * <p>The UI for the control is made up of a series of SpinnerList controls wrapped inside |
| * a SpinnerListContainer that show the |
| * currently-selected date. Through touch or mouse interaction, users |
| * can adjust the selected date.</p> |
| * |
| * <p>The DateSpinnerSkin only defines some sizing properties. |
| * To change the appearance of the DateSpinner control, you typically reskin the underlying |
| * SpinnerListSkin or SpinnerListContainerSkin.</p> |
| * |
| * @mxml <p>The <code><s:DateSpinner></code> tag inherits all of the tag |
| * attributes of its superclass and adds the following tag attributes:</p> |
| * |
| * <pre> |
| * <s:DateSpinner |
| * <strong>Properties</strong> |
| * displayMode="date|time|dateAndTime" |
| * maxDate="null" |
| * minDate="null" |
| * minuteStepSize="1" |
| * selectedDate="" |
| * |
| * <strong>Styles</strong> |
| * accentColor="0x0099FF" |
| * /> |
| * </pre> |
| * |
| * @see spark.components.SpinnerList |
| * @see spark.components.calendarClasses.DateSpinnerItemRenderer |
| * @see spark.skins.mobile.DateSpinnerSkin |
| * |
| * @includeExample examples/DateSpinnerExample.mxml -noswf |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| |
| public class DateSpinner extends SkinnableComponent |
| { |
| //-------------------------------------------------------------------------- |
| // |
| // Class constants |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Specifies to the <code>createDateItemList()</code> method that the list is for showing |
| * years. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected static const YEAR_ITEM:String = "yearItem"; |
| |
| /** |
| * Specifies to the <code>createDateItemList()</code> method that the list is for showing |
| * months. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected static const MONTH_ITEM:String = "monthItem"; |
| |
| /** |
| * Specifies to the <code>createDateItemList()</code> method that the list is for showing |
| * dates of the month or year. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected static const DATE_ITEM:String = "dateItem"; |
| |
| /** |
| * Specifies to the <code>createDateItemList()</code> method that the list is for showing |
| * hours. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected static const HOUR_ITEM:String = "hourItem"; |
| |
| /** |
| * Specifies to the <code>createDateItemList()</code> method that the list is for showing |
| * minutes. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected static const MINUTE_ITEM:String = "minuteItem"; |
| |
| /** |
| * Specifies to the <code>createDateItemList()</code> method that the list is for showing |
| * meridian options. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected static const MERIDIAN_ITEM:String = "meridianItem"; |
| |
| private static const MS_IN_DAY:Number = 1000 * 60 * 60 * 24; |
| |
| // choosing January 1980 to guarantee 31 days in the month |
| private static const JAN1980_IN_MS:Number = 315561660000; |
| |
| // meridian |
| private static const AM:String = "am"; |
| private static const PM:String = "pm"; |
| |
| // default min/max date |
| private static const MIN_DATE_DEFAULT:Date = new Date(DateTimeFormatterEx.MIN_YEAR, 0, 1); |
| private static const MAX_DATE_DEFAULT:Date = new Date(9999, 11, 31, 23, 59, 59, 999); |
| |
| // Emphasized constant |
| mx_internal static const EMPHASIZED_PROPERTY_NAME:String = "_emphasized_"; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Constructor |
| // |
| //-------------------------------------------------------------------------- |
| /** |
| * Constructor. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| public function DateSpinner() |
| { |
| super(); |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Variables |
| // |
| //-------------------------------------------------------------------------- |
| |
| private var dispatchValueCommitEvent:Boolean = false; |
| private var dispatchChangeEvent:Boolean = false; |
| |
| private var populateYearDataProvider:Boolean = true; |
| private var populateMonthDataProvider:Boolean = true; |
| private var populateDateDataProvider:Boolean = true; |
| private var populateHourDataProvider:Boolean = true; |
| private var populateMinuteDataProvider:Boolean = true; |
| private var populateMeridianDataProvider:Boolean = true; |
| |
| mx_internal var refreshDateTimeFormatter:Boolean = true; |
| |
| // the internal DateTimeFormatter that provides a set of extended functionalities |
| mx_internal var dateTimeFormatterEx:DateTimeFormatterEx = new DateTimeFormatterEx(); |
| |
| private var dateTimeFormatter:DateTimeFormatter = new DateTimeFormatter(); |
| |
| // the DateTimeFormatterEx that uses MMMEEEd skeleton pattern to identify |
| // the longest dateList item in DATE_AND_TIME mode |
| private var dayMonthDateFormatter:DateTimeFormatterEx; |
| |
| // the NumberFormatter to identify the longest yearList item in DATE mode |
| private var numberFormatter:NumberFormatter; |
| |
| // caching the longest digit (the array value) between index and 9, inclusive, and refresh when locale changes |
| private var longestDigitArray:Array = new Array(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1); |
| |
| private var dateObj:Date = new Date(); |
| private var use24HourTime:Boolean; |
| |
| // store the longest dateList item and updates only when locale changes |
| private var longestDateItem:Object; |
| private var longestYearItem:Object; |
| |
| // controls whether we should snap to or animate to spinner values |
| private var useAnimationToSetSelectedDate:Boolean = false; |
| |
| // keep track of which lists are currently animating for programmatic animations |
| private var listsBeingAnimated:Dictionary = new Dictionary(); |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Skin parts |
| // |
| //-------------------------------------------------------------------------- |
| |
| [SkinPart] |
| /** |
| * The default factory for creating SpinnerList interfaces for all fields. |
| * This is used by the <code>createDateItemList()</code> method. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| public var dateItemList:IFactory; |
| |
| [SkinPart] |
| /** |
| * The container for the date part lists. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| public var listContainer:IVisualElementContainer; |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Properties |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * The SpinnerList that shows the year field of the date. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected var yearList:SpinnerList; |
| |
| /** |
| * The SpinnerList that shows the month field of the date. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected var monthList:SpinnerList; |
| |
| /** |
| * The SpinnerList that shows the date field of the date. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected var dateList:SpinnerList; |
| |
| /** |
| * The SpinnerList that shows the hour field of the date. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected var hourList:SpinnerList; |
| |
| /** |
| * The SpinnerList that shows the minutes field of the date. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected var minuteList:SpinnerList; |
| |
| /** |
| * The SpinnerList that shows the meridian field (AM/PM) of the date. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| protected var meridianList:SpinnerList; |
| |
| //---------------------------------- |
| // displayMode |
| //---------------------------------- |
| |
| private var _displayMode:String = DateSelectorDisplayMode.DATE; |
| |
| private var displayModeChanged:Boolean = false; |
| |
| [Inspectable(category="General", enumeration="date,time,dateAndTime", defaultValue="date")] |
| |
| /** |
| * Mode the DateSpinner is currently using for display. You can set this value to the constants defined in the |
| * DateSelectorDisplayMode class, or to their string equivalents. |
| * |
| * <p>The following table describes the possible values: |
| * <table class="innertable"> |
| * <tr><th>Mode (String equivalent)</th><th>Description</th></tr> |
| * <tr><td><code>DateSelectorDisplayMode.DATE</code> ("date")</td><td>Displays the month, day, and year. This is the default mode.</td></tr> |
| * <tr><td><code>DateSelectorDisplayMode.TIME</code> ("time")</td><td>Displays the day of week, month, day, and time.</td></tr> |
| * <tr><td><code>DateSelectorDisplayMode.DATE_AND_TIME</code> ("dateAndTime")</td><td>Displays the hours and minutes, and, if the locale supports it, the AM/PM selector.</td></tr> |
| * </table> |
| * </p> |
| * |
| * @see spark.components.calendarClasses.DateSelectorDisplayMode |
| * |
| * @default DateSelectorDisplayMode.DATE |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| public function get displayMode():String |
| { |
| return _displayMode; |
| } |
| |
| /** |
| * @private |
| */ |
| public function set displayMode(value:String):void |
| { |
| if (_displayMode == value || value == null) |
| return; |
| |
| _displayMode = value; |
| displayModeChanged = true; |
| |
| if (value == DateSelectorDisplayMode.TIME) |
| { |
| populateHourDataProvider = true; |
| populateMinuteDataProvider = true; |
| populateMeridianDataProvider = true; |
| } |
| else if (value == DateSelectorDisplayMode.DATE_AND_TIME) |
| { |
| populateYearDataProvider = true; |
| populateMonthDataProvider = true; |
| populateDateDataProvider = true; |
| populateHourDataProvider = true; |
| populateMinuteDataProvider = true; |
| populateMeridianDataProvider = true; |
| } |
| else // default mode is DATE |
| { |
| populateYearDataProvider = true; |
| populateMonthDataProvider = true; |
| populateDateDataProvider = true; |
| } |
| |
| syncSelectedDate = true; // force lists to spin to current selected date |
| |
| invalidateProperties(); |
| } |
| |
| //---------------------------------- |
| // maxDate |
| //---------------------------------- |
| |
| private var _maxDate:Date = new Date(MAX_DATE_DEFAULT.time); |
| |
| private var maxDateChanged:Boolean = false; |
| |
| /** |
| * Maximum selectable date; only this date and dates before this date are selectable. |
| * |
| * <p>Note that the Date object returned by the getter is a copy of |
| * the internal Date, and the one provided to the setter is copied |
| * internally, so modifications to these will not be reflected in the |
| * DateSpinner.</p> |
| * |
| * @default Dec 31st, 9999 |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| public function get maxDate():Date |
| { |
| return new Date(_maxDate.time); |
| } |
| |
| /** |
| * @private |
| */ |
| public function set maxDate(value:Date):void |
| { |
| // don't allow minDate to be outside of the defaults |
| if (value && |
| (value.time < MIN_DATE_DEFAULT.time || value.time > MAX_DATE_DEFAULT.time)) |
| value = null; |
| |
| // ignore if no change |
| if ((_maxDate && value && _maxDate.time == value.time) |
| || (_maxDate == null && value == null)) |
| return; |
| |
| _maxDate = new Date(value != null ? value.time : MAX_DATE_DEFAULT.time); |
| populateYearDataProvider = true; |
| populateDateDataProvider = true; |
| maxDateChanged = true; |
| syncSelectedDate = true; |
| |
| invalidateProperties(); |
| } |
| |
| //---------------------------------- |
| // minDate |
| //---------------------------------- |
| |
| private var _minDate:Date = new Date(MIN_DATE_DEFAULT.time); |
| |
| private var minDateChanged:Boolean = false; |
| |
| /** |
| * Minimum selectable date; only this date and dates after this date are selectable. |
| * |
| * <p>Note that the Date object returned by the getter is a copy of |
| * the internal Date, and the one provided to the setter is copied |
| * internally, so modifications to these will not be reflected in the |
| * DateSpinner.</p> |
| * |
| * <p>The value of the minDate property's year must be greater than or equal to 1601 because |
| * the DateTimeFormatter class supports only the range from 1601 to 30827.</p> |
| * |
| * @default January 1st, 1601 |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| public function get minDate():Date |
| { |
| return new Date(_minDate.time); |
| } |
| |
| /** |
| * @private |
| */ |
| public function set minDate(value:Date):void |
| { |
| // don't allow minDate to be outside of the defaults |
| if (value && |
| (value.time < MIN_DATE_DEFAULT.time || value.time > MAX_DATE_DEFAULT.time)) |
| value = null; |
| |
| // ignore if no change |
| if ((_minDate && value && _minDate.time == value.time) |
| || (_minDate == null && value == null)) |
| return; |
| |
| _minDate = new Date(value != null ? value.time : MIN_DATE_DEFAULT.time); |
| populateYearDataProvider = true; |
| populateDateDataProvider = true; |
| minDateChanged = true; |
| syncSelectedDate = true; |
| |
| invalidateProperties(); |
| } |
| |
| //---------------------------------- |
| // minuteStepSize |
| //---------------------------------- |
| private var _minuteStepSize:int = 1; |
| |
| private var minuteStepSizeChanged:Boolean = false; |
| |
| /** |
| * Minute interval to be used when displaying minutes. Only |
| * applicable in TIME and DATE_AND_TIME modes. Valid values must |
| * be evenly divisible into 60; invalid values will revert to |
| * the default interval of 1. For example, a value of "15" will show |
| * the values 0, 15, 30, 45. |
| * |
| * @default 1 |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| */ |
| public function get minuteStepSize():int |
| { |
| return _minuteStepSize; |
| } |
| |
| /** |
| * @private |
| */ |
| public function set minuteStepSize(value:int):void |
| { |
| // adjust for invalid values |
| if (value <= 0 || 60 % value != 0) |
| value = 1; |
| |
| // short-circuit if no change |
| if (value == _minuteStepSize) |
| return; |
| |
| _minuteStepSize = value; |
| minuteStepSizeChanged = true; |
| populateMinuteDataProvider = true; |
| syncSelectedDate = true; |
| |
| invalidateProperties(); |
| } |
| |
| //---------------------------------- |
| // selectedDate |
| //---------------------------------- |
| private var _selectedDate:Date = todayDate; |
| |
| // set to true initially so that lists will be set to right values on creation |
| private var syncSelectedDate:Boolean = true; |
| |
| [Bindable(event="valueCommit")] |
| /** |
| * Date that is currently selected in the DateSpinner control. |
| * |
| * <p>Note that the Date object returned by the getter is a copy of |
| * the internal Date, and the one provided to the setter is copied |
| * internally, so modifications to these will not be reflected in the |
| * DateSpinner. To modify the DateSpinner's date, get and modify the |
| * current selectedDate and then re-set the selectedDate.</p> |
| * |
| * @default the current date |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| * |
| */ |
| public function get selectedDate():Date |
| { |
| return new Date(_selectedDate.time); |
| } |
| |
| /** |
| * @private |
| */ |
| public function set selectedDate(value:Date):void |
| { |
| // no-op if null; there must always be a selectedDate |
| if (value == null) |
| value = todayDate; |
| |
| // short-circuit if no change |
| if (value.time == _selectedDate.time) |
| return; |
| |
| _selectedDate = new Date(value.time); |
| syncSelectedDate = true; |
| |
| dispatchValueCommitEvent = true; |
| |
| invalidateProperties(); |
| } |
| |
| mx_internal function animateToSelectedDate(value:Date):void |
| { |
| // no-op if null; there must always be a selectedDate |
| if (value == null) |
| value = todayDate; |
| |
| // short-circuit if no change |
| if (value.time == _selectedDate.time) |
| return; |
| |
| selectedDate = value; |
| |
| useAnimationToSetSelectedDate = true; |
| } |
| |
| //---------------------------------- |
| // todayDate |
| //---------------------------------- |
| |
| mx_internal static var _todayDate:Date = null; |
| |
| /** |
| * @private |
| * Function to retrieve the current Date. Provided so that |
| * testing can override by setting the _todayDate variable. |
| */ |
| private static function get todayDate():Date |
| { |
| if (_todayDate != null) |
| { |
| // use a copy, not the original that was passed in |
| return new Date(_todayDate.time); |
| } |
| |
| return new Date(); |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Overridden methods |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @private |
| */ |
| override protected function attachSkin():void |
| { |
| super.attachSkin(); |
| |
| displayModeChanged = true; |
| |
| invalidateProperties(); |
| } |
| |
| /** |
| * @private |
| */ |
| override protected function commitProperties():void |
| { |
| super.commitProperties(); |
| |
| var localeStr:String = getStyle("locale"); |
| if (refreshDateTimeFormatter) |
| { |
| if (localeStr) |
| { |
| dateTimeFormatterEx.setStyle("locale", localeStr); |
| dateTimeFormatter.setStyle("locale", localeStr); |
| } |
| else |
| { |
| dateTimeFormatterEx.clearStyle("locale"); |
| dateTimeFormatter.clearStyle("locale"); |
| } |
| |
| use24HourTime = dateTimeFormatterEx.getUse24HourFlag(); |
| refreshDateTimeFormatter = false; |
| } |
| |
| // stop any animations if we might be resetting the visible values |
| if (syncSelectedDate) |
| stopAllAnimations(); |
| |
| // ================================================== |
| // switch out lists if the display mode changed |
| |
| if (displayModeChanged) |
| { |
| setupDateItemLists(); |
| |
| displayModeChanged = false; |
| syncSelectedDate = true; |
| } |
| |
| // ================================================== |
| // populate the lists with the appropriate data providers |
| |
| populateDateItemLists(localeStr); |
| |
| // ================================================== |
| |
| // correct any integrity violations |
| if (minDateChanged || maxDateChanged || syncSelectedDate || minuteStepSizeChanged) |
| { |
| // check min <= max |
| if (_minDate.time > _maxDate.time) |
| { |
| // note assumption here that we're not using the defaults since they |
| // should always maintain minDate < maxDate integrity |
| |
| // correct min/max dates, one day apart |
| if (!maxDateChanged) |
| _minDate.time = _maxDate.time - MS_IN_DAY; // min date was changed past max |
| else |
| _maxDate.time = _minDate.time + MS_IN_DAY; // max date was changed past min |
| } |
| |
| // make sure there's at least one minuteStepSize between the min and max |
| if ((_maxDate.time - _minDate.time) < (minuteStepSize * 60 * 1000)) |
| _maxDate.time = _minDate.time + (minuteStepSize * 60 * 1000); |
| |
| var origSelectedDate:Date = new Date(_selectedDate.time); |
| |
| // check minDate <= selectedDate <= maxDate |
| if (!_selectedDate || _selectedDate.time < _minDate.time) |
| { |
| _selectedDate = new Date(_minDate.time); |
| } |
| else if (_selectedDate.time > _maxDate.time) |
| { |
| _selectedDate = new Date(_maxDate.time); |
| } |
| |
| minDateChanged = false; |
| maxDateChanged = false; |
| |
| if (minuteStepSizeChanged) |
| { |
| // verify minutes are a multiple of minuteStepSize |
| if ((_selectedDate.minutes % minuteStepSize) != 0) |
| { |
| // adjust to the closest number evenly disible by minuteStepSize |
| _selectedDate.minutes = Math.round(_selectedDate.minutes / minuteStepSize) * minuteStepSize; |
| |
| // last adjustment to make sure we didn't accidentally go outside of bounds |
| if (_selectedDate.time < _minDate.time) |
| _selectedDate.minutes += minuteStepSize; |
| else if (_selectedDate.time > _maxDate.time) |
| _selectedDate.minutes -= minuteStepSize; |
| } |
| |
| minuteStepSizeChanged = false; |
| } |
| |
| if (origSelectedDate.time != _selectedDate.time) |
| dispatchValueCommitEvent = true; |
| |
| disableInvalidSpinnerValues(_selectedDate); |
| } |
| |
| // ================================================== |
| // update selections on the lists if necessary |
| if (syncSelectedDate) |
| { |
| updateListsToSelectedDate(useAnimationToSetSelectedDate); |
| syncSelectedDate = false; |
| |
| if (dispatchChangeEvent || dispatchValueCommitEvent) |
| { |
| // dispatch the events: now or after animation? |
| if (useAnimationToSetSelectedDate) |
| { |
| // we animated the list ourselves; wait for lists |
| // to report VALUE_COMMIT before dispatching our events |
| var numEls:int = listContainer.numElements; |
| var sl:SpinnerList; |
| for (var elIdx:int = 0; elIdx < numEls; elIdx++) |
| { |
| sl = listContainer.getElementAt(elIdx) as SpinnerList; |
| sl.addEventListener(FlexEvent.VALUE_COMMIT, waitForSpinnerListValueCommit_handler); |
| } |
| } |
| else |
| { |
| // dispatch our events immediately |
| dispatchSelectedDateChangedEvents(); |
| } |
| } |
| } |
| |
| // reset flags |
| useAnimationToSetSelectedDate = false; |
| } |
| |
| /** |
| * @private |
| */ |
| override public function styleChanged(styleProp:String):void |
| { |
| super.styleChanged(styleProp); |
| |
| // if locale changed, all date item formats need to be regenerated |
| if (styleProp == "locale") |
| { |
| displayModeChanged = true; // order of lists may be different with new locale |
| |
| populateYearDataProvider = true; |
| populateMonthDataProvider = true; |
| populateDateDataProvider = true; |
| populateHourDataProvider = true; |
| populateMinuteDataProvider = true; |
| populateMeridianDataProvider = true; |
| |
| refreshDateTimeFormatter = true; |
| |
| syncSelectedDate = true; |
| |
| longestDateItem = null; |
| longestYearItem = null; |
| |
| invalidateProperties(); |
| } |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Methods |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Create a list object for the specified date part. |
| * |
| * @param datePart Use date part constants, e.g. YEAR_ITEM. |
| * @param itemIndex Index of the date part in the overall list container. |
| * @param itemCount Number of date parts being shown. |
| * |
| * @langversion 3.0 |
| * @playerversion AIR 3 |
| * @productversion Flex 4.6 |
| * |
| * @return A SpinnerList that contains the part. |
| */ |
| protected function createDateItemList(datePart:String, itemIndex:int, itemCount:int):SpinnerList |
| { |
| return SpinnerList(createDynamicPartInstance("dateItemList")); |
| } |
| |
| /** |
| * @private |
| * Sets up the date item lists based on the current mode. Clears pre-existing lists. |
| */ |
| private function setupDateItemLists():void |
| { |
| // an array of the list and position objects that will be sorted by position |
| var fieldPositionObjArray:ArrayCollection = new ArrayCollection(); |
| var listSort:ISort = new Sort(); |
| listSort.fields = [new SortField("position")]; |
| fieldPositionObjArray.sort = listSort; |
| |
| // clean out the container and all existing lists |
| // they may be in different positions, which will affect how they |
| // need to be (re)created |
| cleanContainer(); |
| |
| var fieldPosition:int = 0; |
| var listItem:Object; |
| var tempList:SpinnerList; |
| var numItems:int; |
| |
| // configure the correct lists to use |
| if (displayMode == DateSelectorDisplayMode.TIME || |
| displayMode == DateSelectorDisplayMode.DATE_AND_TIME) |
| { |
| fieldPositionObjArray.addItem(generateFieldPositionObject(HOUR_ITEM, dateTimeFormatterEx.getHourPosition())); |
| fieldPositionObjArray.addItem(generateFieldPositionObject(MINUTE_ITEM, dateTimeFormatterEx.getMinutePosition())); |
| |
| if (displayMode == DateSelectorDisplayMode.DATE_AND_TIME) |
| fieldPositionObjArray.addItem(generateFieldPositionObject(DATE_ITEM, dateTimeFormatterEx.getMonthPosition())); |
| |
| if (!use24HourTime) |
| fieldPositionObjArray.addItem(generateFieldPositionObject(MERIDIAN_ITEM, dateTimeFormatterEx.getAmPmPosition())); |
| |
| // sort fieldPosition objects by position |
| fieldPositionObjArray.refresh(); |
| |
| numItems = fieldPositionObjArray.length; |
| |
| for each (listItem in fieldPositionObjArray) |
| { |
| switch(listItem.dateItem) |
| { |
| case HOUR_ITEM: |
| { |
| hourList = createDateItemList(HOUR_ITEM, fieldPosition++, numItems); |
| tempList = hourList; |
| break; |
| } |
| case MINUTE_ITEM: |
| { |
| minuteList = createDateItemList(MINUTE_ITEM, fieldPosition++, numItems); |
| tempList = minuteList; |
| break; |
| } |
| case MERIDIAN_ITEM: |
| { |
| meridianList = createDateItemList(MERIDIAN_ITEM, fieldPosition++, numItems); |
| tempList = meridianList; |
| break; |
| } |
| case DATE_ITEM: |
| { |
| dateList = createDateItemList(DATE_ITEM, fieldPosition++, numItems); |
| dateList.wrapElements = false; |
| tempList = dateList; |
| break; |
| } |
| } |
| if (tempList && listContainer) |
| { |
| tempList.addEventListener(IndexChangeEvent.CHANGE, dateItemList_changeHandler); |
| listContainer.addElement(tempList); |
| } |
| } |
| } |
| else // default case: DATE mode |
| { |
| fieldPositionObjArray.addItem(generateFieldPositionObject(MONTH_ITEM, dateTimeFormatterEx.getMonthPosition())); |
| fieldPositionObjArray.addItem(generateFieldPositionObject(DATE_ITEM, dateTimeFormatterEx.getDayOfMonthPosition())); |
| fieldPositionObjArray.addItem(generateFieldPositionObject(YEAR_ITEM, dateTimeFormatterEx.getYearPosition())); |
| |
| // sort fieldPosition objects by position |
| fieldPositionObjArray.refresh(); |
| |
| numItems = fieldPositionObjArray.length; |
| |
| for each (listItem in fieldPositionObjArray) |
| { |
| switch(listItem.dateItem) |
| { |
| case MONTH_ITEM: |
| { |
| monthList = createDateItemList(MONTH_ITEM, fieldPosition++, numItems); |
| tempList = monthList; |
| break; |
| } |
| case DATE_ITEM: |
| { |
| dateList = createDateItemList(DATE_ITEM, fieldPosition++, numItems); |
| tempList = dateList; |
| break; |
| } |
| case YEAR_ITEM: |
| { |
| yearList = createDateItemList(YEAR_ITEM, fieldPosition++, numItems); |
| yearList.wrapElements = false; |
| tempList = yearList; |
| break; |
| } |
| } |
| if (tempList && listContainer) |
| { |
| tempList.addEventListener(IndexChangeEvent.CHANGE, dateItemList_changeHandler); |
| listContainer.addElement(tempList); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Populate the currently existing date item lists with correct data providers using the |
| * provided locale to format the date text correctly. |
| */ |
| private function populateDateItemLists(localeStr:String):void |
| { |
| var today:Date = todayDate; |
| |
| // populate lists that are being shown |
| if (yearList && populateYearDataProvider) |
| { |
| yearList.dataProvider = new YearProvider(localeStr, _minDate.fullYear, |
| _maxDate.fullYear, today); |
| |
| // set size to longest string |
| if (!longestYearItem) |
| longestYearItem = findLongestYearItem(); |
| |
| yearList.typicalItem = longestYearItem; |
| |
| alignList(yearList); |
| } |
| if (monthList && populateMonthDataProvider) |
| { |
| monthList.dataProvider = generateMonths(today); |
| |
| // set size |
| monthList.typicalItem = getLongestLabel(monthList.dataProvider); |
| |
| alignList(monthList); |
| } |
| if (dateList && populateDateDataProvider) |
| { |
| if (displayMode == DateSelectorDisplayMode.DATE_AND_TIME) |
| { |
| dateList.dataProvider = new DateAndTimeProvider(localeStr, _minDate, _maxDate, |
| today); |
| |
| // set size to longest string |
| if (!longestDateItem) |
| longestDateItem = findLongestDateItem(); |
| |
| dateList.typicalItem = longestDateItem; |
| } |
| else |
| { |
| dateList.dataProvider = generateMonthOfDates(today); |
| |
| // set size to width of longest visible value |
| dateList.typicalItem = getLongestLabel(dateList.dataProvider); |
| } |
| |
| alignList(dateList); |
| } |
| if (hourList && populateHourDataProvider) |
| { |
| hourList.dataProvider = generateHours(use24HourTime); |
| hourList.typicalItem = getLongestLabel(hourList.dataProvider); |
| |
| alignList(hourList); |
| } |
| if (minuteList && populateMinuteDataProvider) |
| { |
| minuteList.dataProvider = generateMinutes(); |
| minuteList.typicalItem = getLongestLabel(minuteList.dataProvider); |
| |
| alignList(minuteList); |
| } |
| if (meridianList && populateMeridianDataProvider) |
| { |
| var amObject:Object = generateAmPm(AM); |
| var pmObject:Object = generateAmPm(PM); |
| meridianList.dataProvider = new ArrayCollection([amObject, pmObject]); |
| meridianList.typicalItem = getLongestLabel(meridianList.dataProvider); |
| |
| alignList(meridianList); |
| } |
| |
| // reset all flags |
| populateYearDataProvider = false; |
| populateMonthDataProvider = false; |
| populateDateDataProvider = false; |
| populateHourDataProvider = false; |
| populateMinuteDataProvider = false; |
| populateMeridianDataProvider = false; |
| } |
| |
| // set the selected index on the SpinnerList. use animation only if requested |
| private function goToIndex(list:SpinnerList, newIndex:int, useAnimation:Boolean):void |
| { |
| // don't do anything if it's already on that index |
| if (list.selectedIndex == newIndex) |
| return; |
| |
| if (useAnimation) |
| { |
| list.animateToSelectedIndex(newIndex); |
| listsBeingAnimated[list] = true; |
| } |
| else |
| { |
| list.selectedIndex = newIndex; |
| } |
| } |
| |
| // generate objects to populate a SpinnerList with months |
| private function generateMonths(today:Date):IList |
| { |
| var ac:ArrayCollection = new ArrayCollection(); |
| var todayMonth:int = today.getMonth(); |
| |
| var monthNames:Vector.<String> = dateTimeFormatterEx.getMonthNames(); |
| |
| for (var i:Number = 0; i < 12; i++) |
| { |
| var item:Object = generateDateItemObject(monthNames[i], i); |
| |
| if (i == todayMonth) |
| item[EMPHASIZED_PROPERTY_NAME] = true; |
| |
| ac.addItem(item); |
| } |
| |
| return ac; |
| } |
| |
| // generate objects to populate a SpinnerList with dates, e.g. "1, 2, 3, ... 31" |
| private function generateMonthOfDates(today:Date):IList |
| { |
| var ac:ArrayCollection = new ArrayCollection(); |
| var todayDate:int = today.getDate(); |
| |
| dateTimeFormatter.dateTimePattern = dateTimeFormatterEx.getDayOfMonthPattern(); |
| |
| // guarantee 31 days in the month |
| dateObj.time = JAN1980_IN_MS; |
| |
| for (var i:int = 1; i <= 31; i++) |
| { |
| dateObj.date = i; |
| var item:Object = generateDateItemObject(dateTimeFormatter.format(dateObj), i); |
| |
| if (i == todayDate) |
| item[EMPHASIZED_PROPERTY_NAME] = true; |
| |
| ac.addItem(item); |
| } |
| |
| return ac; |
| } |
| |
| // generate hour objects for a SpinnerList |
| private function generateHours(use24HourTime:Boolean):IList |
| { |
| var ac:ArrayCollection = new ArrayCollection(); |
| |
| var minHour:int = use24HourTime ? 0 : 1; |
| var maxHour:int = use24HourTime ? 23 : 12; |
| |
| dateTimeFormatter.dateTimePattern = dateTimeFormatterEx.getHourPattern(); |
| |
| for (var i:int = minHour; i <= maxHour; i++) |
| { |
| dateObj.hours = i; |
| ac.addItem( generateDateItemObject(dateTimeFormatter.format(dateObj), i) ); |
| } |
| |
| return ac; |
| } |
| |
| private function generateMinutes():IList |
| { |
| var ac:ArrayCollection = new ArrayCollection(); |
| |
| dateTimeFormatter.dateTimePattern = dateTimeFormatterEx.getMinutePattern(); |
| |
| for (var i:int = 0; i <= 59; i += minuteStepSize) |
| { |
| dateObj.minutes = i; |
| ac.addItem( generateDateItemObject(dateTimeFormatter.format(dateObj), i) ); |
| } |
| |
| return ac; |
| } |
| |
| private function generateAmPm(value:String):Object |
| { |
| dateTimeFormatter.dateTimePattern = dateTimeFormatterEx.getAmPmPattern(); |
| |
| dateObj.hours = (value == AM)? 1 : 13; |
| |
| return generateDateItemObject(dateTimeFormatter.format(dateObj), value); |
| } |
| |
| private function findDateItemIndexInDataProvider(item:Number, dataProvider:IList):int |
| { |
| for (var i:int = 0; i < dataProvider.length; i++) |
| { |
| if (dataProvider.getItemAt(i).data == item) |
| return i; |
| } |
| return -1; |
| } |
| |
| private function getLongestLabel(list:IList):Object |
| { |
| var idx:int = -1; |
| var labelWidth:Number; |
| |
| // Even if all 0 because font not embedded correctly, want to return 0 as the longest |
| // so the typicalItem is non-null. |
| var maxWidth:Number = -1; |
| |
| for (var i:int = 0; i < list.length; i++) |
| { |
| // note: measureText() measures UITextField while our labels will be |
| // shown in StyleableTextField, but that's okay because we're only |
| // looking for the text that is relatively longest in the list |
| labelWidth = measureText(list[i].label).width; |
| if (labelWidth > maxWidth) |
| { |
| maxWidth = labelWidth; |
| idx = i; |
| } |
| } |
| if (idx != -1) |
| return list.getItemAt(idx); |
| |
| // The case where the list is empty. This shouldn't happen. |
| return {}; |
| } |
| |
| // set textAlign on list based on position, listType, or typicalItem content type: |
| // left list => align right |
| // right list => align left |
| // HOUR list => align right |
| // MINUTE list => align left |
| // numeric or (numeric + text) => align right |
| // (text + numeric) => align left |
| // all other cases => align center |
| mx_internal function alignList(list:SpinnerList):void |
| { |
| // set outside alignments tight; set alignments for HOUR and MINUTE |
| if (list == listContainer.getElementAt(0)) |
| list.setStyle("textAlign", "right"); |
| else if (list == listContainer.getElementAt(listContainer.numElements - 1)) |
| list.setStyle("textAlign", "left"); |
| else if (list == hourList) |
| list.setStyle("textAlign", "right"); |
| else if (list == minuteList) |
| list.setStyle("textAlign", "left"); |
| else |
| { |
| var testString:String = list.typicalItem["label"]; |
| if (testString == null) |
| { |
| // nothing to test; use default |
| list.setStyle("textAlign", "center"); |
| return; |
| } |
| |
| var numericPattern:RegExp = /^\d+$/; |
| var numericWithTextRight:RegExp = /^\d+\D+$/; |
| var numericWithTextLeft:RegExp = /^\D+\d+$/; |
| if (numericPattern.test(testString) || numericWithTextRight.test(testString)) // e.g. ja-JP month |
| list.setStyle("textAlign", "right"); |
| else if (numericWithTextLeft.test(testString)) |
| list.setStyle("textAlign", "left"); |
| else |
| list.setStyle("textAlign", "center"); // default |
| } |
| } |
| |
| private function updateListsToSelectedDate(useAnimation:Boolean):void |
| { |
| var newIndex:int; |
| if (yearList) |
| { |
| dateTimeFormatter.dateTimePattern = dateTimeFormatterEx.getYearPattern(); |
| newIndex = yearList.dataProvider.getItemIndex( generateDateItemObject(dateTimeFormatter.format(_selectedDate), _selectedDate.fullYear) ); |
| goToIndex(yearList, newIndex, useAnimation); |
| } |
| |
| if (monthList) |
| goToIndex(monthList, _selectedDate.month, useAnimation); |
| |
| if (dateList) |
| { |
| if (displayMode == DateSelectorDisplayMode.DATE) |
| { |
| goToIndex(dateList, _selectedDate.date - 1, useAnimation); |
| } |
| else // DATE_AND_TIME mode |
| { |
| newIndex = dateList.dataProvider.getItemIndex( generateDateItemObject(dayMonthDateFormatter.format(_selectedDate), _selectedDate.time) ); |
| goToIndex(dateList, newIndex, useAnimation); |
| } |
| } |
| if (hourList) |
| { |
| newIndex = use24HourTime ? _selectedDate.hours : (_selectedDate.hours + 11) % 12; |
| goToIndex(hourList, newIndex, useAnimation); |
| } |
| if (minuteList) |
| { |
| newIndex = findDateItemIndexInDataProvider(_selectedDate.minutes, minuteList.dataProvider); |
| goToIndex(minuteList, newIndex, useAnimation); |
| } |
| if (!use24HourTime && meridianList) |
| { |
| newIndex = _selectedDate.hours < 12 ? 0 : 1; |
| goToIndex(meridianList, newIndex, useAnimation); |
| } |
| } |
| |
| // modify existing date item spinner list data providers to mark |
| // as disabled any combinations that could result by moving one spinner |
| // where the resulting new date would be invalid, either by definition |
| // (e.g. Apr 31 is not a valid date) or by limitation (e.g. outside of the |
| // range defined by minDate and maxDate) |
| private function disableInvalidSpinnerValues(thisDate:Date):void |
| { |
| var tempDate:Date; |
| |
| // disable dates in spinners that are invalid (e.g. Apr 31) or fall outside |
| // of the min/max date range |
| if (dateList) |
| { |
| if (displayMode == DateSelectorDisplayMode.DATE) |
| { |
| var listData:IList = dateList.dataProvider; |
| tempDate = new Date(thisDate.time); |
| |
| // run through the entire list of dates and set enabled flags as necessary |
| var cd:CalendarDate = new CalendarDate(thisDate); |
| var numDaysInMonth:int = cd.numDaysInMonth; |
| var listLength:int = listData.length; |
| |
| var minMonthMatch:Boolean = (tempDate.fullYear == _minDate.fullYear |
| && tempDate.month == _minDate.month); |
| var maxMonthMatch:Boolean = (tempDate.fullYear == _maxDate.fullYear |
| && tempDate.month == _maxDate.month); |
| |
| for (var i:int = 0; i < listLength; i++) |
| { |
| var curObj:Object = listData[i]; |
| |
| // is the date invalid for this month? |
| var newEnabledValue:Boolean = i < numDaysInMonth; |
| |
| if (newEnabledValue) |
| { |
| // test for outside min/max range |
| tempDate.date = curObj.data; |
| |
| if (minMonthMatch && tempDate.date < _minDate.date) |
| newEnabledValue = false; |
| if (maxMonthMatch && tempDate.date > _maxDate.date) |
| newEnabledValue = false; |
| } |
| |
| // note this is where future support for more complex unselectable dates could be added |
| |
| if (curObj[SpinnerList.ENABLED_PROPERTY_NAME] != newEnabledValue) |
| { |
| var o:Object = generateDateItemObject(curObj.label, curObj.data, newEnabledValue); |
| o[EMPHASIZED_PROPERTY_NAME] = curObj[EMPHASIZED_PROPERTY_NAME]; |
| listData[i] = o; |
| } |
| } |
| } |
| } |
| |
| // disable months that fall outside of the min/max dates |
| if (monthList && displayMode == DateSelectorDisplayMode.DATE) |
| { |
| tempDate = new Date(thisDate.time); |
| tempDate.date = 1; // avoid accidental rollover to next month |
| |
| listData = monthList.dataProvider; |
| for (i = 0; i < 12; i++) |
| { |
| newEnabledValue = true; |
| |
| tempDate.month = i; |
| if ((tempDate.fullYear == _minDate.fullYear |
| && tempDate.month < _minDate.month) || |
| (tempDate.fullYear == _maxDate.fullYear |
| && tempDate.month > _maxDate.month)) |
| newEnabledValue = false; |
| |
| curObj = listData[i]; |
| if (curObj[SpinnerList.ENABLED_PROPERTY_NAME] != newEnabledValue) |
| { |
| o = generateDateItemObject(curObj.label, curObj.data, newEnabledValue); |
| o[EMPHASIZED_PROPERTY_NAME] = curObj[EMPHASIZED_PROPERTY_NAME]; |
| listData[i] = o; |
| } |
| } |
| } |
| |
| |
| // disable hours that fall outside of the min/max dates |
| if (hourList && (displayMode == DateSelectorDisplayMode.TIME || displayMode == DateSelectorDisplayMode.DATE_AND_TIME)) |
| { |
| tempDate = new Date(thisDate.time); |
| listData = hourList.dataProvider; |
| listLength = listData.length; |
| |
| var minDateMatch:Boolean = (tempDate.fullYear == _minDate.fullYear |
| && tempDate.month == _minDate.month |
| && tempDate.date == _minDate.date); |
| var maxDateMatch:Boolean = (tempDate.fullYear == _maxDate.fullYear |
| && tempDate.month == _maxDate.month |
| && tempDate.date == _maxDate.date); |
| |
| for (i = 0; i < listLength; i++) |
| { |
| curObj = listData[i]; |
| |
| newEnabledValue = true; |
| |
| if (!use24HourTime) |
| tempDate.hours = (i + 1) % 12 + ((_selectedDate.hours >= 12)? 12 : 0); |
| else |
| tempDate.hours = i; |
| |
| if (minDateMatch && tempDate.hours < _minDate.hours) |
| newEnabledValue = false; |
| |
| if (maxDateMatch && tempDate.hours > _maxDate.hours) |
| newEnabledValue = false; |
| |
| if (curObj[SpinnerList.ENABLED_PROPERTY_NAME] != newEnabledValue) |
| { |
| o = generateDateItemObject(curObj.label, curObj.data, newEnabledValue); |
| o[EMPHASIZED_PROPERTY_NAME] = curObj[EMPHASIZED_PROPERTY_NAME]; |
| listData[i] = o; |
| } |
| } |
| } |
| |
| // disable minutes that fall outside of the min/max dates |
| if (minuteList && (displayMode == DateSelectorDisplayMode.TIME || displayMode == DateSelectorDisplayMode.DATE_AND_TIME)) |
| { |
| tempDate = new Date(thisDate.time); |
| listData = minuteList.dataProvider; |
| listLength = listData.length; |
| |
| var minHourMatch:Boolean = (tempDate.fullYear == _minDate.fullYear |
| && tempDate.month == _minDate.month |
| && tempDate.date == _minDate.date |
| && tempDate.hours == _minDate.hours); |
| var maxHourMatch:Boolean = (tempDate.fullYear == _maxDate.fullYear |
| && tempDate.month == _maxDate.month |
| && tempDate.date == _maxDate.date |
| && tempDate.hours == _maxDate.hours); |
| |
| for (i = 0; i < listLength; i++) |
| { |
| curObj = listData[i]; |
| |
| newEnabledValue = true; |
| |
| tempDate.minutes = curObj.data; |
| |
| if (minHourMatch && tempDate.minutes < _minDate.minutes) |
| newEnabledValue = false; |
| |
| if (maxHourMatch && tempDate.minutes > _maxDate.minutes) |
| newEnabledValue = false; |
| |
| if (curObj[SpinnerList.ENABLED_PROPERTY_NAME] != newEnabledValue) |
| { |
| o = generateDateItemObject(curObj.label, curObj.data, newEnabledValue); |
| o[EMPHASIZED_PROPERTY_NAME] = curObj[EMPHASIZED_PROPERTY_NAME]; |
| listData[i] = o; |
| } |
| } |
| } |
| } |
| |
| // clean out the container: remove all elements, detach event listeners, null out references |
| private function cleanContainer():void |
| { |
| if (listContainer) |
| listContainer.removeAllElements(); |
| |
| if (yearList) |
| { |
| yearList.removeEventListener(IndexChangeEvent.CHANGE, dateItemList_changeHandler); |
| yearList = null; |
| } |
| if (monthList) |
| { |
| monthList.removeEventListener(IndexChangeEvent.CHANGE, dateItemList_changeHandler); |
| monthList = null; |
| } |
| if (dateList) |
| { |
| dateList.removeEventListener(IndexChangeEvent.CHANGE, dateItemList_changeHandler); |
| dateList = null; |
| } |
| if (hourList) |
| { |
| hourList.removeEventListener(IndexChangeEvent.CHANGE, dateItemList_changeHandler); |
| hourList = null; |
| } |
| if (minuteList) |
| { |
| minuteList.removeEventListener(IndexChangeEvent.CHANGE, dateItemList_changeHandler); |
| minuteList = null; |
| } |
| if (meridianList) |
| { |
| meridianList.removeEventListener(IndexChangeEvent.CHANGE, dateItemList_changeHandler); |
| meridianList = null; |
| } |
| } |
| |
| // convenience method to generate the standard object format for data in the list dataproviders |
| private function generateDateItemObject(label:String, data:*, enabled:Boolean = true):Object |
| { |
| var obj:Object = { label:label, data:data }; |
| obj[SpinnerList.ENABLED_PROPERTY_NAME] = enabled; |
| return obj; |
| } |
| |
| // generate the fieldPosition object that contains the date part name and position based on locale |
| private function generateFieldPositionObject(datePart:String, position:int):Object |
| { |
| var obj:Object = { dateItem:datePart, position:position }; |
| return obj; |
| } |
| |
| // returns true if any of the lists are currently animating |
| private function get spinnersAnimating():Boolean |
| { |
| if (!listContainer) |
| return false; |
| |
| var len:int = listContainer.numElements; |
| for (var i:int = 0; i < len; i++) |
| { |
| var list:SpinnerList = listContainer.getElementAt(i) as SpinnerList; |
| // return true as soon as we have one list still in touch interaction |
| if (list && list.scroller && list.scroller.inTouchInteraction) |
| return true; |
| } |
| return false; |
| } |
| |
| // identify the dateList item that has the longest width in DATE_AND_TIME mode |
| private function findLongestDateItem():Object |
| { |
| updateDayMonthDateFormatter(); |
| |
| var labelWidth:Number = -1; |
| var maxWidth:Number = 0; |
| dateObj.date = 1; |
| dateObj.hours = 11; |
| dateObj.minutes = 0; |
| dateObj.seconds = 0; |
| dateObj.milliseconds = 0; |
| |
| // 1) find the longest month |
| var longestMonth:int = 0; |
| dateTimeFormatter.dateTimePattern = dayMonthDateFormatter.getMonthPattern(); |
| |
| for (var month:int = 0; month < 12; month++) |
| { |
| dateObj.month = month; |
| |
| labelWidth = measureText(dateTimeFormatter.format(dateObj)).width; |
| if (labelWidth > maxWidth) |
| { |
| maxWidth = labelWidth; |
| longestMonth = month; |
| } |
| } |
| |
| dateObj.month = longestMonth; |
| |
| // 2) find the longest date |
| var longestDate:int = 0; |
| var longestDateWidth:Number; |
| maxWidth = 0; |
| updateNumberFormatter(); |
| |
| for (var date:int = 1; date <= 28; date++) |
| { |
| labelWidth = measureText(numberFormatter.format(date)).width; |
| if (labelWidth > maxWidth) |
| { |
| maxWidth = labelWidth; |
| longestDate = date; |
| } |
| } |
| |
| longestDateWidth = maxWidth; |
| dateObj.date = longestDate; |
| |
| // 3) find the longest date item |
| var longestDateItemObj:Object = dateList.dataProvider.getItemAt(0); |
| var formattedDateStr:String; |
| maxWidth = 0; |
| |
| for (var year:int = 2000; year <= 2010; year++) |
| { |
| dateObj.fullYear = year; |
| formattedDateStr = dayMonthDateFormatter.format(dateObj); |
| labelWidth = measureText(formattedDateStr).width; |
| if (labelWidth > maxWidth) |
| { |
| maxWidth = labelWidth; |
| longestDateItemObj = generateDateItemObject(formattedDateStr, dateObj.time); |
| } |
| } |
| |
| // 4) compare to date 29, 30, 31 |
| var trueLongestDate:int = longestDate; |
| maxWidth = longestDateWidth; |
| |
| for (date = 29; date <= 31; date++) |
| { |
| labelWidth = measureText(numberFormatter.format(date)).width; |
| if (labelWidth > maxWidth) |
| { |
| maxWidth = labelWidth; |
| trueLongestDate = date; |
| } |
| } |
| |
| if (trueLongestDate != longestDate) |
| { |
| formattedDateStr = String(longestDateItemObj.label); |
| |
| // replace all the occurrence of the previous longest date to the true longest date |
| longestDateItemObj.label = String(longestDateItemObj.label).replace(new RegExp(numberFormatter.format(longestDate), "g"), |
| numberFormatter.format(trueLongestDate)); |
| |
| // since the result can be wrong date such as "Wed, Feb 31," assign null to data |
| longestDateItemObj.data = null; |
| } |
| |
| return longestDateItemObj; |
| } |
| |
| // identify the yearList item that has the longest width in DATE mode |
| // the range of year should be from 1601 to 9999 |
| private function findLongestYearItem():Object |
| { |
| // instantiate the number formatter and set its locale to the DateSpinner's locale |
| // and refresh longestDigitArray to pick the longest digit in a current locale format. |
| updateNumberFormatter(); |
| |
| // generate the longest width year |
| var longestYear:int = getLongestDigit(1) * 1000; |
| |
| // if the first (left-most) digit is greater than 1, then the rest digit is just the longest digit of 0~9. |
| if (getLongestDigit(1) > 1) |
| longestYear += getLongestDigit() * 111; |
| |
| // if the first digit is 1, then the second digit should be greater than or equal to |
| // (1) 7 if none of last two digits is 0. |
| // (2) 6 if both last two digits are 0. |
| else |
| longestYear += ((getLongestDigit() == 0)? getLongestDigit(7) : getLongestDigit(6)) * 100 + |
| getLongestDigit() * 10 + getLongestDigit(); |
| |
| dateObj.fullYear = longestYear; |
| dateTimeFormatter.dateTimePattern = dateTimeFormatterEx.getYearPattern(); |
| return generateDateItemObject(dateTimeFormatter.format(dateObj), longestYear); |
| } |
| |
| // return the longest digit between min and 9, inclusive |
| // call updateNumberFormatter() before calling this function to refresh longestDigitArray |
| private function getLongestDigit(min:int = 0):int |
| { |
| var longestDigit:int = longestDigitArray[min]; |
| var labelWidth:Number = -1; |
| var maxWidth:Number = 0; |
| |
| if (longestDigit == -1) |
| { |
| for (var i:int = min; i < 10; i++) |
| { |
| labelWidth = measureText(numberFormatter.format(i)).width; |
| if (labelWidth > maxWidth) |
| { |
| maxWidth = labelWidth; |
| longestDigit = i; |
| } |
| } |
| |
| longestDigitArray[min] = longestDigit; |
| } |
| |
| return longestDigit; |
| } |
| |
| // instantiate the number formatter and update its locale property |
| private function updateNumberFormatter():void |
| { |
| if(!numberFormatter) |
| numberFormatter = new NumberFormatter(); |
| |
| // reset to -1 |
| for (var i:int = 0; i < longestDigitArray.length; i++) |
| longestDigitArray[i] = -1; |
| |
| var localeStr:String = getStyle("locale"); |
| if (localeStr) |
| numberFormatter.setStyle("locale", localeStr); |
| else |
| numberFormatter.clearStyle("locale"); |
| } |
| |
| // instantiate the dayMonthDateFormatter and update its locale property |
| private function updateDayMonthDateFormatter():void |
| { |
| if (!dayMonthDateFormatter) |
| { |
| dayMonthDateFormatter = new DateTimeFormatterEx(); |
| dayMonthDateFormatter.dateTimeSkeletonPattern = DateTimeFormatterEx.DATESTYLE_MMMEEEd; |
| } |
| |
| var localeStr:String = getStyle("locale"); |
| if (localeStr) |
| dayMonthDateFormatter.setStyle("locale", localeStr); |
| else |
| dayMonthDateFormatter.clearStyle("locale"); |
| } |
| |
| // used to delay the dispatch of change events from this DateSpinner until |
| // the underlying SpinnerLists have stopped animating (signified by |
| // a VALUE_COMMIT event) |
| private function waitForSpinnerListValueCommit_handler(event:FlexEvent):void |
| { |
| // if listsBeingAnimated contains event.target remove it |
| if (listsBeingAnimated[event.target]) |
| { |
| delete listsBeingAnimated[event.target]; |
| } |
| |
| // if we're still waiting on any lists, don't dispatch yet |
| for (var key:Object in listsBeingAnimated) |
| { |
| return; |
| } |
| |
| // if no more in listsBeingAnimated, dispatch |
| dispatchSelectedDateChangedEvents(); |
| |
| // clean up |
| var numEls:int = listContainer.numElements; |
| var sl:SpinnerList; |
| for (var elIdx:int = 0; elIdx < numEls; elIdx++) |
| { |
| sl = listContainer.getElementAt(elIdx) as SpinnerList; |
| sl.removeEventListener(FlexEvent.VALUE_COMMIT, waitForSpinnerListValueCommit_handler); |
| } |
| } |
| |
| // dispatch the appropriate events when selectedDate changed |
| private function dispatchSelectedDateChangedEvents():void |
| { |
| if (dispatchChangeEvent) |
| { |
| if (hasEventListener(Event.CHANGE)) |
| dispatchEvent(new Event(Event.CHANGE)); |
| |
| dispatchChangeEvent = false; |
| } |
| if (dispatchValueCommitEvent) |
| { |
| if (hasEventListener(FlexEvent.VALUE_COMMIT)) |
| dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT)); |
| |
| dispatchValueCommitEvent = false; |
| } |
| } |
| |
| private function stopAllAnimations():void |
| { |
| if (spinnersAnimating) |
| { |
| var len:int = listContainer.numElements; |
| for (var i:int = 0; i < len; i++) |
| { |
| var list:SpinnerList = listContainer.getElementAt(i) as SpinnerList; |
| // return true as soon as we have one list still in touch interaction |
| if (list && list.scroller && list.scroller.inTouchInteraction) |
| list.scroller.stopAnimations(); |
| } |
| } |
| } |
| |
| //---------------------------------------------------------------------------------------------- |
| // |
| // Event handlers |
| // |
| //---------------------------------------------------------------------------------------------- |
| /** |
| * Handles changes in the underlying SpinnerLists; applies them to the selectedDate |
| * @param event |
| * |
| */ |
| private function dateItemList_changeHandler(event:IndexChangeEvent):void |
| { |
| if (spinnersAnimating) |
| { |
| // don't commit any changes until all spinners have come to a stop |
| return; |
| } |
| |
| // start with the previous selectedDate |
| var newDate:Date = new Date(_selectedDate.time); |
| |
| var tempDate:Date; |
| var cd:CalendarDate; |
| |
| var numLists:int = listContainer.numElements; |
| var currentList:SpinnerList; |
| |
| var dateRolledBack:Boolean = false; |
| |
| // loop through all lists in the container and adjust selectedDate to their values |
| for (var i:int = 0; i < numLists; i++) |
| { |
| currentList = listContainer.getElementAt(i) as SpinnerList; |
| var newValue:* = currentList.selectedItem; |
| |
| switch (currentList) |
| { |
| case monthList: |
| // rollback date if past end of month |
| if (dateList) |
| { |
| tempDate = new Date(_selectedDate.fullYear, newValue.data, 1); |
| cd = new CalendarDate(tempDate); |
| if (dateList.selectedItem.data > cd.numDaysInMonth) |
| { |
| newDate.date = cd.numDaysInMonth; |
| dateRolledBack = true; |
| } |
| } |
| newDate.month = newValue.data; |
| break; |
| case dateList: |
| // for DATE_AND_TIME mode data is a Date.time value |
| // Must set date before the month to ensure the date is valid for the |
| // month. If newDate is Jan 31 and you set the month to Feb it will change |
| // the month to March since Feb 31 is not a valid date. |
| if (displayMode == DateSelectorDisplayMode.DATE_AND_TIME) |
| { |
| var spinnerDate:Date = new Date(newValue.data); |
| newDate.fullYear = spinnerDate.fullYear; |
| newDate.setMonth(spinnerDate.month, spinnerDate.date); |
| } |
| else if (!dateRolledBack) // don't tamper with date if we already rolled it back |
| { |
| newDate.date = newValue.data; |
| } |
| break; |
| case yearList: |
| // rollback date if past end of month |
| if (dateList) |
| { |
| tempDate = new Date(newValue.data, _selectedDate.month, 1); |
| cd = new CalendarDate(tempDate); |
| if (dateList.selectedItem.data > cd.numDaysInMonth) |
| { |
| newDate.date = cd.numDaysInMonth; |
| dateRolledBack = true; |
| } |
| } |
| newDate.fullYear = newValue.data; |
| break; |
| case hourList: |
| if (use24HourTime) |
| { |
| newDate.hours = newValue.data; |
| } |
| else |
| { |
| // a little trickier; need to convert to 24-hour time |
| // assumption is that if !use24HourTime, meridianList exists |
| newDate.hours = ((newValue.data + 12) % 12) + (meridianList.selectedItem.data == "pm" ? 12 : 0); |
| } |
| break; |
| case minuteList: |
| newDate.minutes = newValue.data; |
| break; |
| case meridianList: |
| if (newValue.data == "am" && newDate.hours > 11) |
| newDate.hours -= 12; |
| else if (newValue.data == "pm" && newDate.hours < 12) |
| newDate.hours += 12; |
| break; |
| default: |
| // unknown list; don't know how to handle |
| break; |
| } |
| } |
| |
| dispatchChangeEvent = true; |
| if (dateRolledBack) |
| animateToSelectedDate(newDate); |
| else |
| selectedDate = newDate; |
| } |
| } |
| } |