Created
August 15, 2014 11:55
-
-
Save rozd/c6877939543e1a3cd5c2 to your computer and use it in GitHub Desktop.
HyperlinkTextBlockTextRenderer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package feathersx.controls.text | |
{ | |
import feathers.controls.text.TextBlockTextRenderer; | |
import flash.geom.Point; | |
import flash.geom.Rectangle; | |
import flash.text.engine.ContentElement; | |
import flash.text.engine.ElementFormat; | |
import flash.text.engine.GroupElement; | |
import flash.text.engine.TextElement; | |
import flash.text.engine.TextLine; | |
import flash.text.engine.TextLineMirrorRegion; | |
import starling.display.Quad; | |
import starling.display.Sprite; | |
import starling.events.Touch; | |
import starling.events.TouchEvent; | |
import starling.events.TouchPhase; | |
public class HyperlinkTextBlockTextRenderer extends TextBlockTextRenderer | |
{ | |
//-------------------------------------------------------------------------- | |
// | |
// Class constants | |
// | |
//-------------------------------------------------------------------------- | |
//------------------------------------- | |
// Class constants: Events | |
//------------------------------------- | |
public static const HYPERLINK:String = "hyperlink"; | |
//------------------------------------- | |
// Class constants: Markups | |
//------------------------------------- | |
public static const HTML_MARKUP:String = "html"; | |
//------------------------------------- | |
// Class constants: States | |
//------------------------------------- | |
public static const LINK_STATE_UP:String = "up"; | |
public static const LINK_STATE_HOVER:String = "hover"; | |
public static const LINK_STATE_DOWN:String = "down"; | |
//------------------------------------- | |
// Class constants: Helpers | |
//------------------------------------- | |
private static const HELPER_POINT:Point = new Point(); | |
//-------------------------------------------------------------------------- | |
// | |
// Class methods | |
// | |
//-------------------------------------------------------------------------- | |
//------------------------------------- | |
// Class methods: Parsers | |
//------------------------------------- | |
protected static const PARSERS:Object = {}; | |
public static function getParserFor(markup:String):Function | |
{ | |
return PARSERS.hasOwnProperty(markup) ? PARSERS[markup] : null; | |
} | |
public static function registerParserFor(markup:String, parser:Function):void | |
{ | |
PARSERS[markup] = parser; | |
} | |
// static initialization | |
{ | |
registerParserFor(HTML_MARKUP, parseHTML); | |
} | |
//------------------------------------- | |
// Class methods: Default parsers | |
//------------------------------------- | |
protected static function parseHTML(text:String):Vector.<ContentElement> | |
{ | |
var hasLinks:Boolean = false; | |
var elements:Vector.<ContentElement> = new <ContentElement>[]; | |
var previousIgnoreWhitespace:Boolean = XML.ignoreWhitespace; | |
var previousPrettyPrinting:Boolean = XML.prettyPrinting; | |
XML.ignoreWhitespace = false; | |
XML.prettyPrinting = false; | |
try | |
{ | |
var xml:XML = new XML("<html>" + text + "</html>"); | |
for each (var child:XML in xml.children()) | |
{ | |
switch (child.nodeKind()) | |
{ | |
case "text" : | |
elements.push(new TextElement(child.toString())); | |
break; | |
case "element" : | |
if (QName(child.name()).localName.toLowerCase() == "a") | |
{ | |
var link:TextElement = new TextElement(child.text()); | |
link.userData = {reference : String(child.@href)}; | |
link.eventMirror = new LinkEventDispatcher(link); | |
elements.push(link); | |
hasLinks = true; | |
} | |
else | |
{ | |
elements.push(new TextElement(child.toXMLString())); | |
} | |
break; | |
} | |
} | |
XML.ignoreWhitespace = previousIgnoreWhitespace; | |
XML.prettyPrinting = previousPrettyPrinting; | |
if (hasLinks) | |
{ | |
return elements; | |
} | |
} | |
catch (error:Error) | |
{ | |
// ignore | |
} | |
return null; | |
} | |
//-------------------------------------------------------------------------- | |
// | |
// Constructor | |
// | |
//-------------------------------------------------------------------------- | |
public function HyperlinkTextBlockTextRenderer() | |
{ | |
super(); | |
this.isQuickHitAreaEnabled = true; | |
this.addEventListener(TouchEvent.TOUCH, touchHandler); | |
} | |
//-------------------------------------------------------------------------- | |
// | |
// Variables | |
// | |
//-------------------------------------------------------------------------- | |
protected var touchPointID:int = -1; | |
protected var currentGlobalX:Number; | |
protected var currentGlobalY:Number; | |
protected var linkBackgroundContainer:Sprite; | |
protected var currentRegion:TextLineMirrorRegion; | |
protected var _groupElement:GroupElement; | |
//-------------------------------------------------------------------------- | |
// | |
// Properties | |
// | |
//-------------------------------------------------------------------------- | |
//------------------------------------ | |
// markup | |
//------------------------------------ | |
private var _markup:String; | |
public function get markup():String | |
{ | |
return _markup; | |
} | |
public function set markup(value:String):void | |
{ | |
if (this._markup == value) | |
{ | |
return; | |
} | |
_markup = value; | |
createContent(); | |
} | |
//------------------------------------ | |
// linkStateNames | |
//------------------------------------ | |
protected var _linkStateNames:Vector.<String> = new <String> | |
[ | |
LINK_STATE_UP, LINK_STATE_DOWN, LINK_STATE_HOVER | |
]; | |
protected function get linkStateNames():Vector.<String> | |
{ | |
return this._linkStateNames; | |
} | |
//------------------------------------ | |
// currentLinkState | |
//------------------------------------ | |
private var _currentLinkState:String; | |
public function get currentLinkState():String | |
{ | |
return this._currentLinkState; | |
} | |
public function set currentLinkState(value:String):void | |
{ | |
if (this._currentLinkState == value) | |
{ | |
return; | |
} | |
if (this.linkStateNames.indexOf(value) < 0) | |
{ | |
throw new ArgumentError("Invalid state: " + value + "."); | |
} | |
this._currentLinkState = value; | |
this.invalidate(INVALIDATION_FLAG_STATE); | |
} | |
//------------------------------------ | |
// linkFormat | |
//------------------------------------ | |
protected var _linkFormat:ElementFormat; | |
public function get linkFormat():ElementFormat | |
{ | |
return this._linkFormat; | |
} | |
public function set linkFormat(value:ElementFormat):void | |
{ | |
if (this._linkFormat == value) return; | |
this._linkFormat = value; | |
this.invalidate(INVALIDATION_FLAG_STYLES); | |
} | |
//------------------------------------ | |
// disabledLinkFormat | |
//------------------------------------ | |
private var _disabledLinkFormat:ElementFormat; | |
public function get disabledLinkFormat():ElementFormat | |
{ | |
return _disabledLinkFormat; | |
} | |
public function set disabledLinkFormat(value:ElementFormat):void | |
{ | |
if (this._disabledLinkFormat == value) return; | |
this._disabledLinkFormat = value; | |
this.invalidate(INVALIDATION_FLAG_STYLES); | |
} | |
//------------------------------------ | |
// stateToLinkBackgroundColorFunction | |
//------------------------------------ | |
protected var _stateToLinkBackgroundColorFunction:Function; | |
public function get stateToLinkBackgroundColorFunction():Function | |
{ | |
return this._stateToLinkBackgroundColorFunction; | |
} | |
public function set stateToLinkBackgroundColorFunction(value:Function):void | |
{ | |
if(this._stateToLinkBackgroundColorFunction == value) | |
{ | |
return; | |
} | |
this._stateToLinkBackgroundColorFunction = value; | |
this.invalidate(INVALIDATION_FLAG_STYLES); | |
} | |
//------------------------------------ | |
// stateToLinkBackgroundAlphaFunction | |
//------------------------------------ | |
protected var _stateToLinkBackgroundAlphaFunction:Function; | |
public function get stateToLinkBackgroundAlphaFunction():Function | |
{ | |
return this._stateToLinkBackgroundAlphaFunction; | |
} | |
public function set stateToLinkBackgroundAlphaFunction(value:Function):void | |
{ | |
if(this._stateToLinkBackgroundAlphaFunction == value) | |
{ | |
return; | |
} | |
this._stateToLinkBackgroundAlphaFunction = value; | |
this.invalidate(INVALIDATION_FLAG_STYLES); | |
} | |
//------------------------------------ | |
// linkBackgroundColor | |
//------------------------------------ | |
private var _linkBackgroundColor:uint; | |
public function get linkBackgroundColor():uint | |
{ | |
return _linkBackgroundColor; | |
} | |
public function set linkBackgroundColor(value:uint):void | |
{ | |
if(this._linkBackgroundColor == value) | |
{ | |
return; | |
} | |
this._linkBackgroundColor = value; | |
this.invalidate(INVALIDATION_FLAG_STYLES); | |
} | |
//------------------------------------ | |
// linkBackgroundAlpha | |
//------------------------------------ | |
private var _linkBackgroundAlpha:Number; | |
public function get linkBackgroundAlpha():Number | |
{ | |
return _linkBackgroundAlpha; | |
} | |
public function set linkBackgroundAlpha(value:Number):void | |
{ | |
if(this._linkBackgroundAlpha == value) | |
{ | |
return; | |
} | |
this._linkBackgroundAlpha = value; | |
this.invalidate(INVALIDATION_FLAG_STYLES); | |
} | |
//------------------------------------ | |
// linkBackgroundGutter | |
//------------------------------------ | |
private var _linkBackgroundGutter:Number; | |
public function get linkBackgroundGutter():Number | |
{ | |
return _linkBackgroundGutter; | |
} | |
public function set linkBackgroundGutter(value:Number):void | |
{ | |
if(this._linkBackgroundGutter == value) | |
{ | |
return; | |
} | |
this._linkBackgroundGutter = value; | |
this.invalidate(INVALIDATION_FLAG_STYLES); | |
} | |
//-------------------------------------------------------------------------- | |
// | |
// Overridden properties | |
// | |
//-------------------------------------------------------------------------- | |
//------------------------------------- | |
// text | |
//------------------------------------- | |
override public function get text():String | |
{ | |
return this._text; | |
} | |
override public function set text(value:String):void | |
{ | |
if (this._text == value) | |
{ | |
return; | |
} | |
this._text = value; | |
createContent(); | |
} | |
//------------------------------------- | |
// content | |
//------------------------------------- | |
override public function set content(value:ContentElement):void | |
{ | |
if(this._content == value) | |
{ | |
return; | |
} | |
if(value is TextElement) | |
{ | |
this._textElement = TextElement(value); | |
} | |
else | |
{ | |
this._textElement = null; | |
} | |
if (value is GroupElement) | |
{ | |
this._groupElement = GroupElement(value); | |
} | |
else | |
{ | |
this._groupElement = null; | |
} | |
this._content = value; | |
this.invalidate(INVALIDATION_FLAG_DATA); | |
} | |
//-------------------------------------------------------------------------- | |
// | |
// Overridden methods | |
// | |
//-------------------------------------------------------------------------- | |
//------------------------------------- | |
// Overridden methods: DisplayObject | |
//------------------------------------- | |
override public function dispose():void | |
{ | |
super.dispose(); | |
this._groupElement = null; | |
} | |
//------------------------------------- | |
// Overridden methods: FeathersControl | |
//------------------------------------- | |
override protected function initialize():void | |
{ | |
super.initialize(); | |
linkBackgroundContainer = new Sprite(); | |
addChildAt(linkBackgroundContainer, 0); | |
} | |
override protected function draw():void | |
{ | |
super.draw(); | |
var stylesInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_STYLES); | |
var dataInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_DATA); | |
var stateInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_STATE); | |
if (dataInvalid || stylesInvalid || stateInvalid) | |
{ | |
refreshLinkBackground(); | |
} | |
} | |
//------------------------------------- | |
// Overridden methods: TextBlockTextRenderer | |
//------------------------------------- | |
override protected function commit():void | |
{ | |
super.commit(); | |
var stylesInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_STYLES); | |
var dataInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_DATA); | |
var stateInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_STATE); | |
if (dataInvalid || stylesInvalid || stateInvalid) | |
{ | |
if (this._groupElement) | |
{ | |
var textFormat:ElementFormat; | |
var linkFormat:ElementFormat; | |
if (!_isEnabled) | |
{ | |
textFormat = _disabledElementFormat || _elementFormat; | |
linkFormat = _disabledLinkFormat || textFormat; | |
} | |
else | |
{ | |
textFormat = _elementFormat; | |
linkFormat = _linkFormat || textFormat; | |
} | |
for (var i:uint = 0, n:uint = _groupElement.elementCount; i < n; i++) | |
{ | |
var element:ContentElement = _groupElement.getElementAt(i); | |
if (element is TextElement) | |
{ | |
var text:TextElement = element as TextElement; | |
if (text.userData && text.userData.hasOwnProperty("reference")) | |
{ | |
text.elementFormat = linkFormat; | |
} | |
else | |
{ | |
text.elementFormat = textFormat; | |
} | |
} | |
} | |
} | |
} | |
} | |
override protected function refreshSnapshot():void | |
{ | |
super.refreshSnapshot(); | |
if (this.linkBackgroundContainer) | |
{ | |
setChildIndex(this.linkBackgroundContainer, 0); | |
} | |
} | |
//-------------------------------------------------------------------------- | |
// | |
// Methods | |
// | |
//-------------------------------------------------------------------------- | |
//------------------------------------- | |
// Methods: Content | |
//------------------------------------- | |
private function createContent():void | |
{ | |
if (this._markup) | |
{ | |
this.createMarkupContent(); | |
} | |
else | |
{ | |
this.createTextContent(); | |
} | |
this.invalidate(INVALIDATION_FLAG_DATA); | |
} | |
private function createTextContent():void | |
{ | |
if (!this._textElement) | |
{ | |
this._textElement = new TextElement(this._text); | |
} | |
else | |
{ | |
this._textElement.text = this._text; | |
} | |
this.content = this._textElement; | |
} | |
private function createMarkupContent():void | |
{ | |
var parser:Function = getParserFor(this._markup); | |
var elements:Vector.<ContentElement>; | |
if (parser != null && (elements = parser(this._text))) | |
{ | |
if (!this._groupElement) | |
{ | |
this._groupElement = new GroupElement(elements); | |
} | |
else | |
{ | |
this._groupElement.setElements(elements); | |
} | |
this.content = this._groupElement; | |
} | |
else | |
{ | |
this.createTextContent(); | |
} | |
} | |
//------------------------------------- | |
// Methods: Link's background | |
//------------------------------------- | |
protected function refreshLinkBackground():void | |
{ | |
this.clearLinkBackground(); | |
if (this.currentRegion != null) | |
{ | |
var backgroundColor:uint; | |
var backgroundAlpha:Number; | |
if (this._stateToLinkBackgroundColorFunction != null) | |
{ | |
backgroundColor = this._stateToLinkBackgroundColorFunction(this, _currentLinkState); | |
} | |
else | |
{ | |
backgroundColor = this._linkBackgroundColor; | |
} | |
if (this._stateToLinkBackgroundAlphaFunction != null) | |
{ | |
backgroundAlpha = this._stateToLinkBackgroundAlphaFunction(this, _currentLinkState); | |
} | |
else | |
{ | |
backgroundAlpha = this._linkBackgroundAlpha; | |
} | |
var nextRegion:TextLineMirrorRegion = this.currentRegion.nextRegion; | |
var prevRegion:TextLineMirrorRegion = this.currentRegion.previousRegion; | |
while (nextRegion) | |
{ | |
drawLinkRegionBackground(nextRegion, backgroundColor, backgroundAlpha); | |
nextRegion = nextRegion.nextRegion; | |
} | |
while (prevRegion) | |
{ | |
drawLinkRegionBackground(prevRegion, backgroundColor, backgroundAlpha); | |
prevRegion = prevRegion.previousRegion; | |
} | |
drawLinkRegionBackground(this.currentRegion, backgroundColor, backgroundAlpha); | |
} | |
} | |
private function clearLinkBackground():void | |
{ | |
if (this.linkBackgroundContainer) | |
{ | |
this.linkBackgroundContainer.removeChildren(); | |
} | |
} | |
private function drawLinkRegionBackground(region:TextLineMirrorRegion, color:uint, alpha:Number):void | |
{ | |
HELPER_POINT.x = region.bounds.x; | |
HELPER_POINT.y = region.bounds.y; | |
var gutter:Number = _linkBackgroundGutter || 0; | |
var point:Point = region.textLine.localToGlobal(HELPER_POINT); | |
var quad:Quad = new Quad(region.bounds.width + gutter * 2, region.bounds.height + gutter * 2, color); | |
quad.x = point.x - gutter; | |
quad.y = point.y - gutter; | |
quad.alpha = alpha; | |
linkBackgroundContainer.addChild(quad); | |
} | |
//------------------------------------- | |
// Methods: Link's handlers | |
//------------------------------------- | |
protected function upLink():void | |
{ | |
this.currentRegion = null; | |
this.clearLinkBackground(); | |
this.currentLinkState = LINK_STATE_UP; | |
} | |
protected function hoverLink(region:TextLineMirrorRegion):void | |
{ | |
if (region != null) | |
{ | |
this.currentRegion = region; | |
this.currentLinkState = LINK_STATE_HOVER; | |
} | |
else | |
{ | |
this.upLink(); | |
} | |
} | |
protected function downLink(region:TextLineMirrorRegion):void | |
{ | |
if (region != null) | |
{ | |
this.currentRegion = region; | |
this.currentLinkState = LINK_STATE_DOWN; | |
} | |
} | |
protected function triggerLink(region:TextLineMirrorRegion):void | |
{ | |
if (region != null) | |
{ | |
if (region.mirror is LinkEventDispatcher) | |
{ | |
var link:TextElement = LinkEventDispatcher(region.mirror).link; | |
if (link && link.userData && link.userData.hasOwnProperty("reference")) | |
{ | |
dispatchEventWith(HYPERLINK, true, link.userData.reference); | |
} | |
} | |
} | |
} | |
//------------------------------------- | |
// Methods: Internal | |
//------------------------------------- | |
protected function getLineMirrorRegionAt(x:Number, y:Number):TextLineMirrorRegion | |
{ | |
HELPER_POINT.x = x; | |
HELPER_POINT.y = y; | |
globalToLocal(HELPER_POINT, HELPER_POINT); | |
for (var i:uint = 0; i < _textLineContainer.numChildren; i++) | |
{ | |
var line:TextLine = _textLineContainer.getChildAt(i) as TextLine; | |
var index:int = line.getAtomIndexAtPoint(HELPER_POINT.x, HELPER_POINT.y); | |
if (index != -1) | |
{ | |
var bounds:Rectangle = line.getAtomBounds(index); | |
for (var j:uint = 0, n:uint = line.mirrorRegions ? line.mirrorRegions.length : 0; j < n; j++) | |
{ | |
var region:TextLineMirrorRegion = line.mirrorRegions[j]; | |
if (region.bounds.containsRect(bounds)) | |
{ | |
return region; | |
} | |
} | |
} | |
} | |
return null; | |
} | |
protected function resetTouchState(touch:Touch = null):void | |
{ | |
this.touchPointID = -1; | |
this.currentGlobalX = this.currentGlobalY = NaN; | |
if (this._isEnabled) | |
{ | |
this.upLink(); | |
} | |
} | |
//-------------------------------------------------------------------------- | |
// | |
// Event handlers | |
// | |
//-------------------------------------------------------------------------- | |
protected function touchHandler(event:TouchEvent):void | |
{ | |
if(!this._isEnabled) | |
{ | |
this.touchPointID = -1; | |
this.currentGlobalX = this.currentGlobalY = NaN; | |
return; | |
} | |
if(this.touchPointID >= 0) | |
{ | |
var touch:Touch = event.getTouch(this, null, this.touchPointID); | |
if(!touch) | |
{ | |
//this should never happen | |
return; | |
} | |
touch.getLocation(this.stage, HELPER_POINT); | |
var isInBounds:Boolean = this.contains(this.stage.hitTest(HELPER_POINT, true)); | |
if(touch.phase == TouchPhase.MOVED) | |
{ | |
if (isInBounds) | |
{ | |
this.downLink(getLineMirrorRegionAt(touch.globalX, touch.globalY)); | |
} | |
else | |
{ | |
this.clearLinkBackground(); | |
} | |
} | |
else if(touch.phase == TouchPhase.ENDED) | |
{ | |
//we we dispatched a long press, then triggered and change | |
//won't be able to happen until the next touch begins | |
if(isInBounds) | |
{ | |
this.triggerLink(getLineMirrorRegionAt(currentGlobalX, currentGlobalY)); | |
} | |
this.resetTouchState(touch); | |
} | |
return; | |
} | |
else //if we get here, we don't have a saved touch ID yet | |
{ | |
touch = event.getTouch(this, TouchPhase.BEGAN); | |
if(touch) | |
{ | |
this.downLink(getLineMirrorRegionAt(touch.globalX, touch.globalY)); | |
this.touchPointID = touch.id; | |
this.currentGlobalX = touch.globalX; | |
this.currentGlobalY = touch.globalY; | |
return; | |
} | |
touch = event.getTouch(this, TouchPhase.HOVER); | |
if(touch) | |
{ | |
this.hoverLink(getLineMirrorRegionAt(touch.globalX, touch.globalY)); | |
return; | |
} | |
//end of hover | |
this.upLink(); | |
} | |
} | |
} | |
} | |
import flash.events.EventDispatcher; | |
import flash.text.engine.ContentElement; | |
import flash.text.engine.ElementFormat; | |
import flash.text.engine.TextElement; | |
class LinkEventDispatcher extends EventDispatcher | |
{ | |
function LinkEventDispatcher(link:TextElement) | |
{ | |
super(); | |
this._link = link; | |
} | |
private var _link:TextElement; | |
public function get link():TextElement | |
{ | |
return _link; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment