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);
@novacoder
Copy link

It doesnt work with Label component.

I use theme and label inside ScrollContainer.

@timoisalive
Copy link

timoisalive commented Oct 20, 2016

Thanks Josh for this, it's somewhat of a help in the ever so difficult field of html text in flash...

I noticed however that the code breaks if you give it text with linebreaks. It will loop indefinitely trying to find the "\r" character from the html text which obviously never is found.

I made a quick fix which seems to work at least for me.

From line 56:

for(var i:int = 0; i <= charIndex; i++)
{
    var regularChar:String = regularText.charAt(i);
    if(regularChar == "\r")
    {
        continue;
    }
    htmlCharIndex++;
    var htmlChar:String = htmlText.charAt(htmlCharIndex);
    do
    {
        ...

@janumedia
Copy link

Hi Josh,
skipTo value in line 76 is possible to return -1 , mean if after "&" has no ";" found the code will do infinite loop. This code should fixed this issue.

if (skipTo > -1)
{
htmlCharIndex = skipTo;
htmlChar = regularChar;
} else
{
htmlChar = htmlText.charAt(htmlCharIndex);
}

@joshtynjala
Copy link
Author

joshtynjala commented Jun 5, 2017

@novacoder You probably need to set isQuickHitAreaEnabled to false on the Label. The default value of true on the Label is an optimization, but it prevents touches from reaching any of its children. I'll add a second example that shows how to use this with a Label.

Sorry for the late response, but apparently, Github doesn't send emails when someone comments on a Gist.

@joshtynjala
Copy link
Author

joshtynjala commented Jun 5, 2017

@timoisalive Thanks for the bug report! HyperlinkTextFieldTextRenderer now handles all types of new lines including \n, \r, and \r\n.

@janumedia Thanks for the bug report! HyperlinkTextFieldTextRenderer no longer gets stuck in an infinite loop if ; does not appear after &. I also handled another special case where it would behave incorrectly with something like &a b; where a space appeared somewhere between the & and ; characters.

Sorry for the late response, guys, but Gist comments don't send me email notifications...

@lucien144
Copy link

Hi @joshtynjala, I found another bug. If you are having a text with multiple links and a new line in between them, the while loop never ends... Example:

"I agree to the <u><a href='http://google.com' target='_blank'>Terms and Conditions</a></u><br>and <u><a href='http://yahoo.com' target='_blank'>Privacy Policy</a></u>\nand <u><a href='http://msn.com' target='_blank'>Privacy Policy</a></u>."

As a possible fix seems to me to close the if conditional with else in the while loop, otherwise it never ends.

do {
	...
	if (htmlChar == "<") {
		...
	} else if (htmlChar == "&") {
		...
	} else {
		htmlChar = regularChar;
	}
}
while (htmlChar != regularChar);

@skolesnyk
Copy link

skolesnyk commented Jun 27, 2017

Doesn't work with any closing tags like p /p, br/ , etc.

@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