Статьи

Flex DateChooser с возможностью выбора недели / месяца


Недавно передо мной была поставлена ​​задача улучшить DateChooser Flex, чтобы включить возможность выбора недели или месяца.
В настоящее время вы можете выбрать неделю или месяц, выделив несколько дней с помощью shift-click (если allowMultipleSelection = true), но мы хотели сделать его немного проще для пользователя.

Вот конечный продукт (нажмите на стрелку или название месяца):
Просмотр источника включен.
Реализация этой функции включала следующее:

  1. Создание класса для представления «стрелки выбора недели»
  2. Расширение класса CalendarLayout
  3. Создание пользовательского события, чтобы указать, когда была выбрана неделя, и помочь нам определить, какая неделя была выбрана
  4. Расширение класса DateChooser
  5. Создание некоторых служебных методов, которые помогут нам определить дату начала / окончания выбранной недели или месяца

В дополнение к предоставлению исходного кода, эта запись блога документирует детали реализации и дает краткое объяснение примененной логики.

Создание класса для представления «стрелки выбора недели»

Начнем с создания стрелки, используемой для выбора недели. Этот класс будет в первую очередь отвечать за рисование стрелки. Тем не менее, он также будет содержать ссылку на номер строки стрелки (это важно при определении того, какие календарные дни следует выбирать программным способом при нажатии стрелки).

package components
{
    import flash.events.MouseEvent;
    import flash.geom.Point;

    import mx.core.UIComponent;

    public class WeekSelector extends UIComponent
    {
        public var weekRow:int;

        public function WeekSelector(weekRow:int)
        {
            super();
            this.weekRow = weekRow;
            height = 5;
            width = 5;
            buttonMode = true;
            useHandCursor = true;
            addEventListener(MouseEvent.MOUSE_UP, onMouseUp)
        }

        private function onMouseUp(event:MouseEvent):void
        {
            // we don't want to interfere with CalendarLayout's MOUSE_UP handler
            event.stopImmediatePropagation();
        }

        override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
        {
            var vertices:Vector.<Number> = new Vector.<Number>;

            var top:Point = new Point(0, 0);
            var bottom:Point = new Point(0, height);
            var right:Point = new Point(width, height / 2);

            vertices.push(top.x, top.y, bottom.x, bottom.y, right.x, right.y);

            graphics.beginFill(0x000000, 1);
            graphics.drawTriangles(vertices);
            graphics.endFill();
        }
    }
}

Расширение класса CalendarLayout

Наш новый макет DateChooser включает стрелки для выбора недели. DateChooser Flex делегирует макет своего календаря экземпляру класса CalendarLayout (см. DateChooser.dateGrid). Поэтому нам нужно будет расширить CalendarLayout и сделать следующее:

  1. Создайте экземпляр WeekSelector (стрелка) для каждой строки календаря
  2. Расположите каждую стрелку слева от соответствующей строки календаря
  3. Зарегистрируйте обработчики кликов на каждой стрелке

Достаточно просто, верно? Конечно, но есть небольшая загвоздка. Для позиционирования стрелок необходимо знать положение каждой строки календаря (значения x & y строки). Эта информация может быть извлечена из свойства dayBlocksArray объекта CalendarLayout, которое помечено как
mx_internal . К счастью, в отличие от приватных переменных, свойства mx_internal доступны для подклассов, поэтому я просто назначил свойство моего подкласса свойству dayBlocksArray в CalendarLayout. Теперь я могу получить доступ к содержимому dayBlocksArray.

package components
{
    import events.WeekSelectionEvent;
    
    import flash.events.MouseEvent;
    import flash.geom.Point;
    
    import mx.controls.CalendarLayout;
    import mx.core.UITextField;
    import mx.core.mx_internal;

    public class CustomCalendarLayout extends CalendarLayout
    {
        private const ROWS_IN_CALENDAR_LAYOUT:int = 5;
        private var weekSelectors:Array = [];
        private var dayBlocksArray:Array = [];

        public function CustomCalendarLayout()
        {
            super();
        }

        override protected function createChildren():void
        {
            super.createChildren();
            createWeekSelectors();
        }

        override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
        {
            super.updateDisplayList(unscaledWidth, unscaledHeight);
            // we need to get a hold of a reference to the superclass' dayBlocksArray property
            // it contains information needed to position each week selection arrow
            dayBlocksArray = mx_internal::dayBlocksArray 
            positionWeekSelectors();
        }

        private function createWeekSelectors():void
        {
            // row zero is [S, M, T, W, T, F, S], we don't need an arrow for that row
            // so let's start with row one
            var weekRowIndex:int = 1; 

            while (weekRowIndex <= ROWS_IN_CALENDAR_LAYOUT)
            {
                // create an arrow for each row
                weekSelectors.push(new WeekSelector(weekRowIndex));
                addChild(weekSelectors[weekRowIndex - 1]);
                weekSelectors[weekRowIndex - 1].addEventListener(MouseEvent.CLICK, onClick);
                weekRowIndex++;
            }
        }

        private function positionWeekSelectors():void
        {
            var weekRowPosition:Point;
            var weekSelector:WeekSelector;
            // again, let's start with row one (no need for the [S, M, T, W, T, F, S] row)
            var weekRowIndex:int = 1;

            while (weekRowIndex <= ROWS_IN_CALENDAR_LAYOUT)
            {
                weekRowPosition = getWeekRowPosition(weekRowIndex);
                weekSelector = weekSelectors[weekRowIndex - 1];
                weekSelector.x = weekRowPosition.x;
                weekSelector.y = weekRowPosition.y;
                setChildIndex(weekSelector, numChildren - 1);
                weekRowIndex++;
            }
        }

        private function getWeekRowPosition(row:int = 0):Point
        {
            // using dayBlocksArray, we can find a UITextField that represents the last day (column 6) in the row
            // and we can use that UITextField's X & Y properties to determine the positioning of the row
            // and ultimately where to position the arrow
            var column:int = 6;
            var day:UITextField = dayBlocksArray[column][row];
            var rowY:Number = day.y + day.height / 3;

            return new Point(3, rowY);
        }

        private function onClick(event:MouseEvent):void
        {
            var dayInWeek:int = getDayInSelectedWeek(event);
            dispatchWeekSelectionEvent(dayInWeek);
        }

        private function getDayInSelectedWeek(event:MouseEvent):int
        {
            var target:WeekSelector = event.target as WeekSelector; // target is the arrow that was clicked
            var row:int = target.weekRow; // the arrow knows which row it belongs to
            var day:UITextField; // the dateChooser calendar is a grid of UITextFields

            const FIRST_DAY_OF_MONTH:int = 1;
            
            // let's loop through each column in the arrow's row until we find a UITextField with text
            // this means - until we find a date. notice how the first row in a calendar sometimes 
            // doesn't have a date at the beginning of the week?

            for (var columnIndex:* in dayBlocksArray)
            {
                day = (dayBlocksArray[columnIndex][row] as UITextField)

                if (day.text && day.text != "")
                {
                    return parseInt(day.text);
                }
            }

            return FIRST_DAY_OF_MONTH;
        }

        private function dispatchWeekSelectionEvent(dayInWeek:int):void
        {
            var weekSelectionEvent:WeekSelectionEvent = new WeekSelectionEvent(WeekSelectionEvent.WEEK_SELECTED, true);
            weekSelectionEvent.dayInWeek = dayInWeek;
            // dispatch the bubbling event that will get caught by our custom datechooser
            // and select all the days for that week
            dispatchEvent(weekSelectionEvent);
        }
    } //EOClass
} //EOPackage

Создание пользовательского события, чтобы указать, когда была выбрана неделя, и помочь нам определить, какая неделя была выбрана

В этой задаче нет ничего особенно интересного. Нам нужно пользовательское событие, которое будет пузыриться, чтобы указать, когда была выбрана неделя. Полезная нагрузка этого события включает день (на самом деле любой день), который является частью выбранной недели. Мы будем использовать этот день, чтобы выяснить даты начала и окончания выбранной недели.

Если вам нужно освежить в памяти при создании пользовательских событий, прочтите «
Примеры Flex — Создание пользовательского события» .

package events
{
    import flash.events.Event;

    public class WeekSelectionEvent extends Event
    {
        public static const WEEK_SELECTED:String = "weekSelected";
        
        protected var _dayInWeek:int;

        public function WeekSelectionEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false)
        {
            super(type, bubbles, cancelable);
        }
        
        override public function clone():Event
        {
            var clone:WeekSelectionEvent = new WeekSelectionEvent(type, bubbles, cancelable);
            clone.dayInWeek = dayInWeek;
            return clone;
        }
        
        public function get dayInWeek():int
        {
            return _dayInWeek;
        }

        public function set dayInWeek(value:int):void
        {
            _dayInWeek = value;
        }
    } //EOClass
} //EOPackage

Extension of the DateChooser class

Пришло время собрать все это вместе. Нам понадобится модифицированная версия DateChooser, чтобы использовать наш пользовательский макет календаря (CustomCalendarLayout), который включает в себя наши изящные маленькие стрелки выбора. Кроме того, мы добавим возможность выбирать весь месяц, просто нажав на метку месяца.

Опять же, нам повезло, что свойства DateChooser, необходимые для выполнения этого обязательства (dateGrid и monthDisplay), доступны через mx_internal. В противном случае мы были бы в мире боли.

package components
{    
    import events.WeekSelectionEvent;
    
    import flash.events.MouseEvent;
    
    import mx.controls.*;
    import mx.core.UITextField;
    import mx.core.UITextFormat;
    import mx.core.mx_internal;
    import mx.events.CalendarLayoutChangeEvent;
    import mx.events.DateChooserEvent;
    import mx.styles.StyleProxy;
    
    import utils.CustomDateChooserUtil;

    public class CustomDateChooser extends DateChooser
    {
        private const FIRST_DAY_OF_MONTH:int = 1;
        
        private var dateGrid:CalendarLayout;

        public function CustomDateChooser()
        {
            super();
            // dispatched from our CustomCalendarLayout when a "week arrow" is clicked
            addEventListener(WeekSelectionEvent.WEEK_SELECTED, selectWeek); 
        }

        override protected function createChildren():void
        {
            overrideAddDateGrid_in_superclass_createChildren();
            super.createChildren();
            addMonthDisplayHandlers();
            addDateGrid();
        }

        private function addMonthDisplayHandlers():void
        {
            // make the date chooser's month display function like a link
            mx_internal::monthDisplay.addEventListener(MouseEvent.CLICK, selectMonth);
            mx_internal::monthDisplay.addEventListener(MouseEvent.MOUSE_OVER, onMouseOverMonthDisplay);
            mx_internal::monthDisplay.addEventListener(MouseEvent.MOUSE_OUT, onMouseOutMonthDisplay);
        }

        private function addDateGrid():void
        {
            addChild(mx_internal::dateGrid);
        }

        private function selectWeek(event:WeekSelectionEvent):void
        {
            // catch the event dispatched when a week selection arrow is clicked
            // and set the DateChooser's selectedRanges property to a value representing the selected week
            var dayInWeek:int = event.dayInWeek as int;
            var selectedDate:Date = new Date(displayedYear, displayedMonth, dayInWeek);
            var weekRange:Object = CustomDateChooserUtil.getWeekOf(selectedDate);
            
            selectedRanges = [weekRange];
        }

        private function selectMonth(event:MouseEvent):void
        {
            // set the DateChooser's selectedRanges property to a value representing the entire displayed month
            var selectedDate:Date = new Date(displayedYear, displayedMonth, FIRST_DAY_OF_MONTH);
            var monthRange:Object = CustomDateChooserUtil.getMonthOf(selectedDate);
            
            selectedRanges = [monthRange];
        }

        private function onMouseOverMonthDisplay(event:MouseEvent):void
        {
            highlightMonthDisplay(event);
        }

        private function onMouseOutMonthDisplay(event:MouseEvent):void
        {
            unHighlightMonthDisplay(event);
        }

        private function highlightMonthDisplay(event:MouseEvent):void
        {
            // bold / underline on rollover (just like a link)
            var monthDisplay:UITextField = event.target as UITextField;
            var textFormat:UITextFormat = new UITextFormat(this.systemManager);
            textFormat.underline = true;
            textFormat.color = 0x0000FF;
            monthDisplay.setTextFormat(textFormat);
        }

        private function unHighlightMonthDisplay(event:MouseEvent):void
        {
            // unbold / remove underline on rollout (just like a link)
            var monthDisplay:UITextField = event.target as UITextField;
            var textFormat:UITextFormat = new UITextFormat(this.systemManager);
            textFormat.underline = false;
            textFormat.color = 0x000000;
            monthDisplay.setTextFormat(textFormat);
        }

        private function overrideAddDateGrid_in_superclass_createChildren():void
        {
            // we're going to assign the DateChooser's dateGrid property an instance of our CustomCalendarLayout
            // fortunately, super.createChildren will not override our assignment. our CustomCalendarLayout is in place.
            dateGrid = new CustomCalendarLayout(); // this is really the only line I care about changing
            // the rest of this method works exactly as the super.createChildren method does
            dateGrid.styleName = new StyleProxy(this, calendarLayoutStyleFilters);
            // however, unfortunately the event handlers in the super class are marked private
            // so i'll have to copy their contents from the super class into this subclass
            dateGrid.addEventListener(CalendarLayoutChangeEvent.CHANGE, copyOf_DateGrid_changeHandler_from_superclass);
            dateGrid.addEventListener(DateChooserEvent.SCROLL, copyOfDateGrid_scrollHandler_from_superclass);
            mx_internal::dateGrid = dateGrid;
        }

        private function copyOf_DateGrid_changeHandler_from_superclass(event:CalendarLayoutChangeEvent):void
        {
            // this is just a copy of the DateChooser.dateGrid_changeHandler 
            // but, i had to use the selectedDate public setter instead of the private variable
            selectedDate = CalendarLayout(event.target).selectedDate; 
            
            var e:CalendarLayoutChangeEvent = new CalendarLayoutChangeEvent(CalendarLayoutChangeEvent.CHANGE);
            e.newDate = event.newDate;
            e.triggerEvent = event.triggerEvent;
            dispatchEvent(e);
        }

        private function copyOfDateGrid_scrollHandler_from_superclass(event:DateChooserEvent):void
        {
            // this is just a copy of the DateChooser.dateGrid_scrollHandler
            dispatchEvent(event);
        }

    } //EOClass
} //EOPackage

Создание некоторых служебных методов, которые помогут нам определить дату начала / окончания выбранной недели или месяца

Вы заметите, что в CustomDateChooser мы использовали два метода, чтобы помочь нам определить дату начала и окончания выбранной недели (getWeekOf) или дату начала и окончания выбранного месяца (getMonthOf). Вот реализация этих методов.

package utils
{
    public class CustomDateChooserUtil
    {
        public static function getWeekOf(selectedDate:Date):Object
        {
            // takes in a date and returns an object representing a start and end date for the selected week
            // in a format that DateChooser.selectedRanges likes
            var weekIndex:int = selectedDate.day;
            var weekStartDate:Date = new Date(selectedDate.fullYear, selectedDate.month, selectedDate.date - weekIndex);
            var weekEndDate:Date = new Date(weekStartDate.fullYear, weekStartDate.month, weekStartDate.date + 6);

            return {rangeStart: weekStartDate, rangeEnd: weekEndDate};
        }

        public static function getMonthOf(selectedDate:Date):Object
        {
            // takes in a date and returns an object representing a start and end date for the selected month
            // in a format that DateChooser.selectedRanges likes
            var monthStartDate:Date = new Date(selectedDate.fullYear, selectedDate.month, 1);

            // Flex interprets day 0 to be the last day of the preceeding month
            var monthEndDate:Date = new Date(selectedDate.fullYear, selectedDate.month + 1, 0);

            return {rangeStart: monthStartDate, rangeEnd: monthEndDate};
        }
    } //EOClass
} //EOPackage

Вот и ты. Я надеюсь, что люди найдут этот компонент полезным. Надо также отдать должное моему коллеге
Полу Сайегу , поскольку мы «спарились» в разработке этого компонента.