scaffold.libs_as.feathers.controls.popups.DropDownPopUpContentManager.as Maven / Gradle / Ivy
/*
Feathers
Copyright 2012-2015 Bowler Hat LLC. All Rights Reserved.
This program is free software. You can redistribute and/or modify it in
accordance with the terms of the accompanying license agreement.
*/
package feathers.controls.popups
{
import feathers.core.IFeathersControl;
import feathers.core.IValidating;
import feathers.core.PopUpManager;
import feathers.core.ValidationQueue;
import feathers.events.FeathersEventType;
import feathers.utils.display.getDisplayObjectDepthFromStage;
import feathers.utils.display.stageToStarling;
import flash.errors.IllegalOperationError;
import flash.events.KeyboardEvent;
import flash.geom.Rectangle;
import flash.ui.Keyboard;
import starling.core.Starling;
import starling.display.DisplayObject;
import starling.display.DisplayObjectContainer;
import starling.display.Stage;
import starling.events.Event;
import starling.events.EventDispatcher;
import starling.events.ResizeEvent;
import starling.events.Touch;
import starling.events.TouchEvent;
import starling.events.TouchPhase;
/**
* Dispatched when the pop-up content opens.
*
* The properties of the event object have the following values:
*
* Property Value
* bubbles
false
* currentTarget
The Object that defines the
* event listener that handles the event. For example, if you use
* myButton.addEventListener()
to register an event listener,
* myButton is the value of the currentTarget
.
* data
null
* target
The Object that dispatched the event;
* it is not always the Object listening for the event. Use the
* currentTarget
property to always access the Object
* listening for the event.
*
*
* @eventType starling.events.Event.OPEN
*/
[Event(name="open",type="starling.events.Event")]
/**
* Dispatched when the pop-up content closes.
*
* The properties of the event object have the following values:
*
* Property Value
* bubbles
false
* currentTarget
The Object that defines the
* event listener that handles the event. For example, if you use
* myButton.addEventListener()
to register an event listener,
* myButton is the value of the currentTarget
.
* data
null
* target
The Object that dispatched the event;
* it is not always the Object listening for the event. Use the
* currentTarget
property to always access the Object
* listening for the event.
*
*
* @eventType starling.events.Event.CLOSE
*/
[Event(name="close",type="starling.events.Event")]
/**
* Displays pop-up content as a desktop-style drop-down.
*/
public class DropDownPopUpContentManager extends EventDispatcher implements IPopUpContentManager
{
/**
* @private
*/
private static const HELPER_RECTANGLE:Rectangle = new Rectangle();
/**
* The pop-up content will be positioned below the source, if possible.
*
* @see #primaryDirection
*/
public static const PRIMARY_DIRECTION_DOWN:String = "down";
/**
* The pop-up content will be positioned above the source, if possible.
*
* @see #primaryDirection
*/
public static const PRIMARY_DIRECTION_UP:String = "up";
/**
* Constructor.
*/
public function DropDownPopUpContentManager()
{
}
/**
* @private
*/
protected var content:DisplayObject;
/**
* @private
*/
protected var source:DisplayObject;
/**
* @inheritDoc
*/
public function get isOpen():Boolean
{
return this.content !== null;
}
/**
* @private
*/
protected var _isModal:Boolean = false;
/**
* Determines if the pop-up will be modal or not.
*
* Note: If you change this value while a pop-up is displayed, the
* new value will not go into effect until the pop-up is removed and a
* new pop-up is added.
*
* In the following example, the pop-up is modal:
*
*
* manager.isModal = true;
*
* @default false
*/
public function get isModal():Boolean
{
return this._isModal;
}
/**
* @private
*/
public function set isModal(value:Boolean):void
{
this._isModal = value;
}
/**
* @private
*/
protected var _overlayFactory:Function;
/**
* If isModal
is true
, this function may be
* used to customize the modal overlay displayed by the pop-up manager.
* If the value of overlayFactory
is null
, the
* pop-up manager's default overlay factory will be used instead.
*
* This function is expected to have the following signature:
* function():DisplayObject
*
* In the following example, the overlay is customized:
*
*
* manager.isModal = true;
* manager.overlayFactory = function():DisplayObject
* {
* var quad:Quad = new Quad(1, 1, 0xff00ff);
* quad.alpha = 0;
* return quad;
* };
*
* @default null
*
* @see feathers.core.PopUpManager#overlayFactory
*/
public function get overlayFactory():Function
{
return this._overlayFactory;
}
/**
* @private
*/
public function set overlayFactory(value:Function):void
{
this._overlayFactory = value;
}
/**
* @private
*/
protected var _gap:Number = 0;
/**
* The space, in pixels, between the source and the pop-up.
*/
public function get gap():Number
{
return this._gap;
}
/**
* @private
*/
public function set gap(value:Number):void
{
this._gap = value;
}
/**
* @private
*/
protected var _primaryDirection:String = PRIMARY_DIRECTION_DOWN;
/**
* The space, in pixels, between the source and the pop-up.
*
* @default DropDownPopUpContentManager.PRIMARY_DIRECTION_DOWN
*
* @see #PRIMARY_DIRECTION_DOWN
* @see #PRIMARY_DIRECTION_UP
*/
public function get primaryDirection():String
{
return this._primaryDirection;
}
/**
* @private
*/
public function set primaryDirection(value:String):void
{
this._primaryDirection = value;
}
/**
* @private
*/
protected var _fitContentMinWidthToOrigin:Boolean = true;
/**
* If enabled, the pop-up content's minWidth
property will
* be set to the width
property of the origin, if it is
* smaller.
*
* @default true
*/
public function get fitContentMinWidthToOrigin():Boolean
{
return this._fitContentMinWidthToOrigin;
}
/**
* @private
*/
public function set fitContentMinWidthToOrigin(value:Boolean):void
{
this._fitContentMinWidthToOrigin = value;
}
/**
* @private
*/
protected var _lastGlobalX:Number;
/**
* @private
*/
protected var _lastGlobalY:Number;
/**
* @inheritDoc
*/
public function open(content:DisplayObject, source:DisplayObject):void
{
if(this.isOpen)
{
throw new IllegalOperationError("Pop-up content is already open. Close the previous content before opening new content.");
}
this.content = content;
this.source = source;
PopUpManager.addPopUp(this.content, this._isModal, false, this._overlayFactory);
if(this.content is IFeathersControl)
{
this.content.addEventListener(FeathersEventType.RESIZE, content_resizeHandler);
}
this.content.addEventListener(Event.REMOVED_FROM_STAGE, content_removedFromStageHandler);
this.layout();
var stage:Stage = this.source.stage;
stage.addEventListener(TouchEvent.TOUCH, stage_touchHandler);
stage.addEventListener(ResizeEvent.RESIZE, stage_resizeHandler);
stage.addEventListener(Event.ENTER_FRAME, stage_enterFrameHandler);
//using priority here is a hack so that objects higher up in the
//display list have a chance to cancel the event first.
var priority:int = -getDisplayObjectDepthFromStage(this.content);
Starling.current.nativeStage.addEventListener(KeyboardEvent.KEY_DOWN, nativeStage_keyDownHandler, false, priority, true);
this.dispatchEventWith(Event.OPEN);
}
/**
* @inheritDoc
*/
public function close():void
{
if(!this.isOpen)
{
return;
}
var content:DisplayObject = this.content;
this.content = null;
this.source = null;
var stage:Stage = content.stage;
stage.removeEventListener(TouchEvent.TOUCH, stage_touchHandler);
stage.removeEventListener(ResizeEvent.RESIZE, stage_resizeHandler);
stage.removeEventListener(Event.ENTER_FRAME, stage_enterFrameHandler);
var starling:Starling = stageToStarling(stage);
starling.nativeStage.removeEventListener(KeyboardEvent.KEY_DOWN, nativeStage_keyDownHandler);
if(content is IFeathersControl)
{
content.removeEventListener(FeathersEventType.RESIZE, content_resizeHandler);
}
content.removeEventListener(Event.REMOVED_FROM_STAGE, content_removedFromStageHandler);
if(content.parent)
{
content.removeFromParent(false);
}
this.dispatchEventWith(Event.CLOSE);
}
/**
* @inheritDoc
*/
public function dispose():void
{
this.close();
}
/**
* @private
*/
protected function layout():void
{
if(this.source is IValidating)
{
IValidating(this.source).validate();
if(!this.isOpen)
{
//it's possible that the source will close its pop-up during
//validation, so we should check for that.
return;
}
}
var sourceWidth:Number = this.source.width;
var hasSetBounds:Boolean = false;
var uiContent:IFeathersControl = this.content as IFeathersControl;
if(this._fitContentMinWidthToOrigin && uiContent && uiContent.minWidth < sourceWidth)
{
uiContent.minWidth = sourceWidth;
hasSetBounds = true;
}
if(this.content is IValidating)
{
uiContent.validate();
}
if(!hasSetBounds && this._fitContentMinWidthToOrigin && this.content.width < sourceWidth)
{
this.content.width = sourceWidth;
}
var stage:Stage = this.source.stage;
//we need to be sure that the source is properly positioned before
//positioning the content relative to it.
var starling:Starling = stageToStarling(stage);
var validationQueue:ValidationQueue = ValidationQueue.forStarling(starling);
if(validationQueue && !validationQueue.isValidating)
{
//force a COMPLETE validation of everything
//but only if we're not already doing that...
validationQueue.advanceTime(0);
}
var globalOrigin:Rectangle = this.source.getBounds(stage);
this._lastGlobalX = globalOrigin.x;
this._lastGlobalY = globalOrigin.y;
var downSpace:Number = (stage.stageHeight - this.content.height) - (globalOrigin.y + globalOrigin.height + this._gap);
//skip this if the primary direction is up
if(this._primaryDirection == PRIMARY_DIRECTION_DOWN && downSpace >= 0)
{
layoutBelow(globalOrigin);
return;
}
var upSpace:Number = globalOrigin.y - this._gap - this.content.height;
if(upSpace >= 0)
{
layoutAbove(globalOrigin);
return;
}
//do what we skipped earlier if the primary direction is up
if(this._primaryDirection == PRIMARY_DIRECTION_UP && downSpace >= 0)
{
layoutBelow(globalOrigin);
return;
}
//worst case: pick the side that has the most available space
if(upSpace >= downSpace)
{
layoutAbove(globalOrigin);
}
else
{
layoutBelow(globalOrigin);
}
//the content is too big for the space, so we need to adjust it to
//fit properly
var newMaxHeight:Number = stage.stageHeight - (globalOrigin.y + globalOrigin.height);
if(uiContent)
{
if(uiContent.maxHeight > newMaxHeight)
{
uiContent.maxHeight = newMaxHeight;
}
}
else if(this.content.height > newMaxHeight)
{
this.content.height = newMaxHeight;
}
}
/**
* @private
*/
protected function layoutAbove(globalOrigin:Rectangle):void
{
var idealXPosition:Number = globalOrigin.x;
var xPosition:Number = this.content.stage.stageWidth - this.content.width;
if(xPosition > idealXPosition)
{
xPosition = idealXPosition;
}
if(xPosition < 0)
{
xPosition = 0;
}
this.content.x = xPosition;
this.content.y = globalOrigin.y - this.content.height - this._gap;
}
/**
* @private
*/
protected function layoutBelow(globalOrigin:Rectangle):void
{
var idealXPosition:Number = globalOrigin.x;
var xPosition:Number = this.content.stage.stageWidth - this.content.width;
if(xPosition > idealXPosition)
{
xPosition = idealXPosition;
}
if(xPosition < 0)
{
xPosition = 0;
}
this.content.x = xPosition;
this.content.y = globalOrigin.y + globalOrigin.height + this._gap;
}
/**
* @private
*/
protected function content_resizeHandler(event:Event):void
{
this.layout();
}
/**
* @private
*/
protected function stage_enterFrameHandler(event:Event):void
{
this.source.getBounds(this.source.stage, HELPER_RECTANGLE);
if(HELPER_RECTANGLE.x != this._lastGlobalX || HELPER_RECTANGLE.y != this._lastGlobalY)
{
this.layout();
}
}
/**
* @private
*/
protected function content_removedFromStageHandler(event:Event):void
{
this.close();
}
/**
* @private
*/
protected function nativeStage_keyDownHandler(event:KeyboardEvent):void
{
if(event.isDefaultPrevented())
{
//someone else already handled this one
return;
}
if(event.keyCode != Keyboard.BACK && event.keyCode != Keyboard.ESCAPE)
{
return;
}
//don't let the OS handle the event
event.preventDefault();
this.close();
}
/**
* @private
*/
protected function stage_resizeHandler(event:ResizeEvent):void
{
this.layout();
}
/**
* @private
*/
protected function stage_touchHandler(event:TouchEvent):void
{
var target:DisplayObject = DisplayObject(event.target);
if(this.content == target || (this.content is DisplayObjectContainer && DisplayObjectContainer(this.content).contains(target)))
{
return;
}
if(this.source == target || (this.source is DisplayObjectContainer && DisplayObjectContainer(this.source).contains(target)))
{
return;
}
if(!PopUpManager.isTopLevelPopUp(this.content))
{
return;
}
//any began touch is okay here. we don't need to check all touches
var stage:Stage = Stage(event.currentTarget);
var touch:Touch = event.getTouch(stage, TouchPhase.BEGAN);
if(!touch)
{
return;
}
this.close();
}
}
}