The Adobe Flex AIR runtime provides an mx:html control that lets you embedded an html control in your RIA (rich internet application). It can be directed to a particular webpage by setting the location property. It is a live browser, the user can then click around on the links they see and navigate to other pages.
I thought it would be nice to wrap the control in a flex component and give it a very simple address bar. The Flex programming model makes it very easy to drop this new component into a flex application. Changes to the component are easily reflected in any application that uses the component.
I wanted the text area of the address bar to meet some special requirements
- When a new web page is navigated to, it should change to reflect the new url
- If the user is editing the address, do not change what is shown in the text input box. i think it's nasty to replace what a user is typing while they are typing it, and I spent a lot of time to figure out how to prevent that.
I also wanted the label for "URL:" in front of the text input area to show some scrolling dots while a page load is in progress.
It took me an evening to figure out how to create a webbrowser component that would meet these requirements. It's one of my earliest examples of a component, so I apologize in advance if I've missed any common Flex stylistic etiquette. (It sure would be nice if Flex included a "pretty printer" to cleanup the indentation of the source code, the mixed spaces and tabs didn't look so great on this wiki page.)
Updates:
- 1.02 - (Not yet released)
- Put in a flex library project "WebmillComponents" com.webmill.air.browserpane
- Goal: Drop in replacement for \<mx:HTML\>
- To use as a component in another project:
- include in the <mx:WindowedApplication - xmlns:wmui="com.webmill.air.browserpane.*"
- Projects -> Properties -> Flex Build Path - Add SWC = WebmillComponents.swc
- Change <mx:HTML> to <wmui:BrowserPane>
- To use as a component in another project:
- 1.01
- Wrapped the buttons in a canvas so they can easily be moved to the top or bottom of the html control
- Actually hookup the go button
- Make all event handlers and most other functions private.
- Add a refresh button, and make sure it starts the scrolling dots.
- Add some comments
Todo:
- Create a location property on the component that external users can set
<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300" initialize="init();">
<mx:Canvas id="addressBar" bottom="0" right="0" left="0">
<mx:Label id="lblURL" text="URL:" bottom="3" left="0"/>
<mx:TextInput id="txtURL" text="http://www.cnn.com"
right="218" left="31" bottom="1"
change="changeInput(event);"
textInput="urlTextInput(event)"
enter="urlEnter(event)"/>
<mx:Button label="Go" id="btnGo" right="177" bottom="1" width="40" click="clickGo()"/>
<mx:Button id="btnRefresh" label="Refresh" click="refresh();" fontSize="7" bottom="1" right="121" width="55"/>
<mx:Button id="btnBack" label="< Back" click="navigateBackward();" fontSize="7" bottom="1" right="68" width="52"/>
<mx:Button id="btnFwd" label="Forward >" click="navigateForward()" fontSize="7" width="66" right="1" bottom="1"/>
</mx:Canvas>
<mx:HTML id="browser" right="0" left="0" top="0" bottom="26"
location="http://www.cnn.com" complete="browserComplete(event)"
locationChange="browserLocationChange(event)"/>
<mx:Script>
<![CDATA[
import flash.events.Event;
import flash.events.TextEvent;
private var sVersion:String = "1.01";
private var bLocationChanging:Boolean = false;
private var DefaultButtonColor:Object;
private var DefaultButtonText:String = "URL:";
private var bUserEditingLocation:Boolean = false;
private function init():void {
// Remember the beginning color and text so we can restore it.
DefaultButtonColor=lblURL.getStyle("color");
DefaultButtonText = lblURL.text;
SetupTimer();
}
private var myTimer:Timer=null;
private var iTimerCount:int = 0;
private function SetupTimer():void {
if (myTimer == null)
{ // Four times a second seems fast enough.
myTimer = new Timer(250);
myTimer.addEventListener("timer", timerHandler);
myTimer.start();
}
}
private function timerHandler(event:TimerEvent):void {
iTimerCount++;
if (this.bLocationChanging)
{ // Have some fun making the asterisks walk, and the color change too.
var S:String = " ";
S=S.substr(0,(iTimerCount % 5)) + "***";
lblURL.setStyle("color","#"+(3+iTimerCount%10).toString()+"C"+
(1+iTimerCount%10).toString()+"7"+
(0+iTimerCount%10).toString()+"B");
lblURL.text = S;
}
// Dynamically enable the forward and back buttons.
btnBack.enabled = (browser.htmlLoader.historyPosition>0);
btnFwd.enabled = (browser.htmlLoader.historyPosition<browser.htmlLoader.historyLength-1);
}
private function IsUserEditingLocation():Boolean {
// It turns out that checking if the user has the focus on the txtURL is a
// great way to figure out if the user is intending to change the addres.
// Cursor keys within the txtURL box don't trigger the regular key event, so
// checking for focus gets around that. In fact, the bUserEditingLocation
// could probably be eliminated in favor of just using the focus.
// Make sure you check that focus is a defined object, or you'll null out.
if (systemManager.stage.focus)
{
if (systemManager.stage.focus.parent == txtURL)
{
trace("User has txtURL focused.");
return true;
}
}
return (bUserEditingLocation);
}
private function setChanging():void {
lblURL.setStyle("color","#3C170B");
lblURL.text = "...";
bLocationChanging = true;
}
private function browserLocationChange(event:Event):void {
setChanging();
if (!IsUserEditingLocation())
{
SetURLTextInput(browser.location);
}
}
private function urlTextInput(event:TextEvent):void {
// trace("urlTextInput event:"+event.text);
if (!this.bUserEditingLocation)
{
trace("Setting bUserEditingLocation = true");
this.bUserEditingLocation =true;
}
}
private function urlEnter(event:Event):void {
trace("urlEnter");
if (browser.location != txtURL.text)
{
trace("bUserEditingLocation = false and setting location to "+txtURL.text);
browser.htmlLoader.cancelLoad();
browser.location = txtURL.text;
bUserEditingLocation = false;
focusManager.setFocus(browser);
}
}
private function changeInput(event:Event):void {
}
private function SetURLTextInput(S:String):void
{
trace("SetURLTextInput to "+S);
txtURL.text = S;
}
private function browserComplete(event:Event):void {
if (txtURL.text != browser.location)
{
if (!IsUserEditingLocation())
{
trace("BrowserComplete setting new location to text input "+ browser.location);
SetURLTextInput(browser.location);
}
else
{
trace("BrowserComplete while user is editing");
}
}
else
{
trace("BrowserComplete to same location");
}
// Stop the marching dots, and return to the default text and color.
bLocationChanging = false;
lblURL.setStyle("color",DefaultButtonColor);
lblURL.text = DefaultButtonText;
}
public function navigateForward():void {
if (browser.htmlLoader.historyPosition<browser.htmlLoader.historyLength-1)
{
var S:String = browser.htmlLoader.getHistoryAt(browser.htmlLoader.historyPosition+1).url;
trace("Next url:"+S);
browser.htmlLoader.cancelLoad();
this.bUserEditingLocation = false;
this.txtURL.text = S;
browser.historyForward();
}
else
{
trace("No next url");
}
}
public function navigateBackward():void {
if (browser.htmlLoader.historyPosition>0)
{ // Flex docs say the function name is "historyAt", but that is incorrect
var S:String = browser.htmlLoader.getHistoryAt(browser.htmlLoader.historyPosition-1).url;
trace("Prev url:"+S);
browser.htmlLoader.cancelLoad();
this.bUserEditingLocation = false;
this.txtURL.text = S;
browser.historyBack();
}
else
{
trace("No previous url");
}
}
private function clickGo():void {
browser.cancelLoad();
bUserEditingLocation = false;
browser.location = txtURL.text;
}
public function refresh():void {
setChanging();
browser.reload();
}
]]>
</mx:Script>
</mx:Canvas>