Skip to content

Instantly share code, notes, and snippets.

@rozd
Created August 15, 2014 11:55
Show Gist options
  • Save rozd/c6877939543e1a3cd5c2 to your computer and use it in GitHub Desktop.
Save rozd/c6877939543e1a3cd5c2 to your computer and use it in GitHub Desktop.
HyperlinkTextBlockTextRenderer
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