Skip to content

Instantly share code, notes, and snippets.

@joshtynjala
Last active July 20, 2017 15:09
Show Gist options
  • Save joshtynjala/7997065 to your computer and use it in GitHub Desktop.
Save joshtynjala/7997065 to your computer and use it in GitHub Desktop.
Feathers TextFieldTextRenderer with Hyperlinks
package feathersx.controls.text
{
import feathers.controls.text.TextFieldTextRenderer;
import flash.geom.Point;
import flash.net.URLRequest;
import flash.net.navigateToURL;
import starling.events.Touch;
import starling.events.TouchEvent;
import starling.events.TouchPhase;
import starling.utils.Pool;
public class HyperlinkTextFieldTextRenderer extends TextFieldTextRenderer
{
//these tags will translate to the \r character
private static const BREAK_CONTENT:Vector.<String> = new <String>
[
"br",
"br/",
"/p",
"li",
"/li",
];
public function HyperlinkTextFieldTextRenderer()
{
this.isHTML = true;
}
override public function set isHTML(value:Boolean):void
{
super.isHTML = value;
if(this._isHTML)
{
this.addEventListener(TouchEvent.TOUCH, touchHandler);
}
else
{
this.removeEventListener(TouchEvent.TOUCH, touchHandler);
}
}
/**
* @private
*/
protected function touchHandler(event:TouchEvent):void
{
if(!this._isHTML)
{
return;
}
var touch:Touch = event.getTouch(this, TouchPhase.ENDED);
if(!touch)
{
return;
}
var location:Point = touch.getLocation(this, Pool.getPoint());
var charIndex:int = this.textField.getCharIndexAtPoint(location.x, location.y);
Pool.putPoint(location);
var htmlCharIndex:int = -1;
var htmlText:String = this._text;
var regularText:String = this.textField.text;
var htmlTextLength:int = htmlText.length;
var lastHTMLContent:String;
for(var i:int = 0; i <= charIndex; i++)
{
htmlCharIndex++;
if(htmlCharIndex >= htmlTextLength)
{
//this shouldn't happen, but there's a chance that the html
//index and the regular index get out of sync, and this is
//better than being in an infinite loop!
break;
}
var regularChar:String = regularText.charAt(i);
var htmlChar:String = htmlText.charAt(htmlCharIndex);
if(regularChar === "\r")
{
if(htmlChar === "\n")
{
//if the html text uses \n, it will be replaced with \r
//in the regular text
continue;
}
else if(htmlChar === "\r")
{
//if the html text uses \r\n, it will be replaced with
//\r in the regular text
//we should also skip the extra \n
htmlCharIndex++;
continue;
}
}
if(htmlChar === "\r")
{
//if the html text uses \r (but not \r\n), it will be
//completely skipped in the regular text
htmlCharIndex++;
htmlChar = htmlText.charAt(htmlCharIndex);
}
var lastHTMLIndex:int = -1;
do
{
if(lastHTMLIndex === htmlCharIndex)
{
//we haven't moved forward at all,
//so we're stuck in an infinite loop!
break;
}
lastHTMLIndex = htmlCharIndex;
if(htmlCharIndex >= htmlTextLength)
{
//we've gone past the end, we must be stuck
//in an infinite loop!
break;
}
if(htmlChar == "<")
{
var skipTo:int = htmlText.indexOf(">", htmlCharIndex);
lastHTMLContent = htmlText.substr(htmlCharIndex + 1, skipTo - htmlCharIndex - 1);
if(regularChar === "\r" && BREAK_CONTENT.indexOf(lastHTMLContent) !== -1)
{
htmlCharIndex = skipTo;
}
else
{
htmlCharIndex = skipTo + 1;
}
htmlChar = htmlText.charAt(htmlCharIndex);
}
else if(htmlChar == "&")
{
skipTo = htmlText.indexOf(";", htmlCharIndex);
var spaceIndex:int = htmlText.indexOf(" ", htmlCharIndex);
if(skipTo !== -1 && (spaceIndex === -1 || spaceIndex > skipTo))
{
//it's possible that there will be no ; after the &
//also, if a space appears before ;, then the & is
//not the start of the entity.
htmlCharIndex = skipTo;
}
htmlChar = regularChar;
}
}
while(htmlChar != regularChar);
}
if(!lastHTMLContent || lastHTMLContent.search(/^a\s+/) != 0)
{
return;
}
var linkStartIndex:int = lastHTMLContent.search(/href=[\"\']/) + 6;
if(linkStartIndex < 2)
{
return;
}
var linkEndIndex:int = lastHTMLContent.indexOf("\"", linkStartIndex + 1);
if(linkEndIndex < 0)
{
linkEndIndex = lastHTMLContent.indexOf("'", linkStartIndex + 1);
if(linkEndIndex < 0)
{
return;
}
}
var url:String = lastHTMLContent.substr(linkStartIndex, linkEndIndex - linkStartIndex);
navigateToURL(new URLRequest(url));
}
}
}
//HyperlinkTextFieldTextRenderer may be used on its own without a parent component
var textRenderer:HyperlinkTextFieldTextRenderer = new HyperlinkTextFieldTextRenderer();
textRenderer.text = "This is a link to <u><a href=\"http://google.com\">Google</a></u>. This is a link to <u><a href=\"http://adobe.com\">Adobe</a></u>.";
this.addChild(textRenderer);
//HyperlinkTextFieldTextRenderer may also be used in a component that supports text renderers
var label:Label = new Label();
//you need to allow hit tests to reach the label's text renderer
//as an optimization, isQuickHitAreaEnabled is true by default, so turn it off
label.isQuickHitAreaEnabled = false;
label.text = "This is a link to <u><a href=\"http://google.com\">Google</a></u>. This is a link to <u><a href=\"http://adobe.com\">Adobe</a></u>.";
label.textRendererFactory = function():ITextRenderer
{
var textRenderer:HyperlinkTextFieldTextRenderer = new HyperlinkTextFieldTextRenderer();
return textRenderer;
};
this.addChild(label);
@joshtynjala
Copy link
Author

joshtynjala commented Jul 20, 2017

@lucien144 @skolesnyk I fixed the infinite loop caused by using <br>, <br/>, <p>, </p>, or <li>.

I also figured out a good way to detect any more unexpected infinite loops, so it should break out of them. Things may end up getting out of sync with the text comparison between regular text and HTML text, but at least it won't freeze!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment