reptile7's JavaScript blog
Monday, June 21, 2010
Linking Down Two Sides
Blog Entry #182

We continue today our discussion of Microsoft's "execCommand Example: Creating a Link" demo. We deconstructed the demo's code in the previous entry and are now ready to adapt the demo to non-MSIE browsers; in doing so, there are three basic issues that we need to address:
(1) How can we verify that the user has selected some text?
(2) For browsers that don't support the second execCommand( ) parameter - and that's all of them on the Mac platform - how do we query the user for a link URL?
(3) How do we confirm that link creation has been successful?
Of these issues, the second is the 'lowest-hanging fruit', so we'll deal with it first.

Your URL, comrade

Point #6 of Mozilla's "Converting an app using document.designMode from IE to Mozilla" article notes a simple and obvious way to solicit a link URL from the user: use a prompt( ) box.

var linkURL = window.prompt("Please enter your link URL into the field below.", "http://");

['Enter the URL' prompt( ) box]

The prompt( ) return, linkURL in this case, can then be fed to the createLink command:

document.execCommand("createLink", false, linkURL);

"But I want a box like Microsoft's box."

Well, in that case you're going to have to create your own custom dialog box, and that's not difficult to do - the basic structure of the box is easy to code, although right-aligning the and buttons takes a bit of CSS patience - but integrating that box with the rest of the script is easier said than done. A ) command can be used to pop up such a box, and an inputted URL can be returned to the opener document by, say, assigning it to the value of an <input type="hidden"> control. But unlike the prompt( ) method, ) does not cause the operating system to block (as in to delay or sit idle while waiting for something): the browser will speedily move on to the createLink command and then either create a link to undefined (Firefox, Opera, Camino) or do nothing at all (Safari, Chrome) because there's no URL value to act on (yet).

However, Microsoft has more recently implemented a showModalDialog( ) method and a returnValue property, both of the window object and now supported to varying degrees by Firefox, Safari, Camino, and Chrome, that in theory would allow us to get around the ) non-blocking problem; indeed, Mozilla has posted here a showModalDialog( )/returnValue demo that does just what we want to do in the present situation. Notwithstanding the broken link in its dialog box, the Mozilla demo works very nicely with Firefox, Safari, and Chrome - it'd also work with MSIE 5.x for the Mac if the latter supported the textContent property - but not so well with Camino, which does open the showModalDialogBox.html window but doesn't system-block at the showModalDialog( ) command in the opener source (the alert(r); command fires, popping up a null message). Oddly, Opera doesn't support the showModalDialog( ) method at all.

Now, I really should flesh out this custom dialog business to a greater extent at this point - particularly so given that the W3C is bringing the showModalDialog( ) method and the returnValue property into HTML5 (nope, these features didn't make it to the HTML5 Recommendation stage) - but as I wanted to concentrate on cross-browser code in this entry, let's just go with the prompt( ) route for the time being.

Are you selected?

Netscape may have begun its approach to selection access with the humble document.getSelection( ) method, but that's certainly not where Mozilla is today in this area. Shortly after the implementation of Microsoft's document.selection object, Netscape/Mozilla developed its own selection object, which was introduced in Netscape 6 - here are the details:

(1) getSelection( ) was moved from the document object to the window object.
(document.getSelection( ) still appears in Mozilla's list of document object methods but there's no separate page for it and Mozilla now maintains a skeletal page for it here. document.getSelection( ) is supported by almost all of the non-MSIE GUI browsers on my computer although it generates a Deprecated method document.getSelection() called. Please use window.getSelection() instead. Error Console message when using Firefox and Camino.)

var selObj = window.getSelection( );

(2) window.getSelection( ) returns not a string but an object implementing a Selection interface comprising a much larger set of properties and methods than that given to Microsoft's selection object. There's no overlap between the Mozilla and Microsoft selection objects: their respective sets of properties and methods are mutually exclusive.

(3) The Mozilla selection object can be variabilized with any valid identifier, including selection, which is not a JavaScript reserved word.

(4) The Mozilla Selection interface has a toString( ) method that [r]eturns a string currently being represented by the selection object, i.e., the currently selected text; the JavaScript engine calls this method automatically when a selection object is used in a context requiring a string. Fortunately, it is not necessary to create a separate Range object to access selection text as it is in Microsoft's case.

Mozilla synopsizes its Selection interface in a DOM selection Reference that is part of the Gecko DOM Reference. The W3C will be bringing the Mozilla Selection interface into HTML5 a Selection API specification.

Getting back to the "Creating a Link" demo, we would therefore use the following Mozilla code to determine whether the user has made a selection or not:
var selObj = window.getSelection( );
if (selObj != "") { // If the user has selected some text:
	...create a link and color its text... }
else window.alert("Please select some text!");
/* In the if condition, selObj stringifies to the selection text; it is not necessary to explicitly call selObj.toString( ), as noted above. */
I am gratified to report that Firefox, Opera, Safari, Camino, and Chrome all support the above code.

Anchor element A-OK

Following Microsoft's design, here's how we might on the Mozilla side test if the createLink command was successfully executed:

Mozilla's Selection interface has a getRangeAt( ) method that can be used to create a standard Range object representing the selection text.

var range = selObj.getRangeAt(0); /* range isn't a JavaScript reserved word either. */

A selection can encompass one or more ranges, which for the getRangeAt( ) method are indexed ordinally à la other script collections. Our My favorite Web site selection holds a single range whose getRangeAt( ) index is 0.

The standard Range object does not have a method corresponding to Microsoft's parentElement( ) method; however, it does have a commonAncestorContainer property that [r]eturns the deepest Node that...encloses a Range:

var rangeAncestor = range.commonAncestorContainer; /* Returns [object Text] in our case */

For My favorite Web site, rangeAncestor would return/point to a Text Node, more specifically, an object implementing the DOM Text interface and representing the My favorite Web site is worth clicking on. Don't forget to check out my favorite music group! text of the demo's second paragraph.

The Text interface inherits from the DOM Node interface a parentNode property that when applied to an element's text returns a Node object corresponding to the element and implementing the DOM Element interface. With an Element Node in hand, we can use the tagName property to check if My favorite Web site now has an anchor element parent:

if (rangeAncestor.parentNode.tagName == "A") { ...color the link text or take some other action... }
else window.alert("Sorry, we are unable to determine whether link creation was successful or not.");

Additional comments

• The range range is bounded by a single (text) node. The standard Range object's startContainer property, which [r]eturns the Node within which the Range starts, or its endContainer property, which [r]eturns the Node within which the Range ends, can therefore be used instead of the commonAncestorContainer property if desired.

• For the text of a link, a linkTextNode.parentNode expression stringifies to the link's href value.

• All of the above in this section holds for Firefox, Safari, Camino, and Chrome, but not quite for Opera, which seems to implement the standard Range object differently than do the former browsers. Without getting into the details, I find when using Opera that I can access the anchor element node with range.startContainer.parentNode but not via the commonAncestorContainer and endContainer Range properties.

No Range required

It's actually not necessary to go through a Range object to get our hands on the My favorite Web site is worth clicking on... text node; Mozilla's Selection interface has an anchorNode property, which [r]eturns the node in which the selection begins, and a focusNode property, which [r]eturns the node in which the selection ends, that will allow us to do that, e.g.:

if (selObj.anchorNode.parentNode.tagName == "A") { ...color the link text or take some other action... }
else window.alert("Sorry, we are unable to determine whether link creation was successful or not.");

Text activation

We noted in the previous post that with MSIE for Windows it is not necessary to 'activate' the second paragraph, via either a document.designMode="on" or contentEditable="true" assignment, to make execCommand( ) changes to it - a good thing in this case as it minimizes the editability of the demo document. Once the code has been cross-browserized, text activation is similarly not required for Opera but is required for Firefox, Safari, Camino, and Chrome to run the demo.

For Safari and Chrome, editability can be effectively minimized by enabling and disabling contentEditable for the second paragraph on the fly prior to the createLink command and after the foreColor command, respectively:
document.getElementById("p1").contentEditable = "true";
document.execCommand("createLink", false, linkURL);
if (selObj.anchorNode.parentNode.tagName == "A") {
	document.execCommand("foreColor", false, "#ff0033");
	document.getElementById("p1").contentEditable = "false"; }
<p id="p1" style="color:#3366cc;">My favorite Web site is worth clicking on...</p>
For Firefox and Camino, editability can be suppressed post-execution but not pre-execution. Curiously, turning contentEditable on for the second paragraph per the above code "collapses" the selection* to a single point when using Firefox and Camino (selObj.isCollapsed returns true) - even more strangely, the collapse point is sent to the beginning of the button element's child text node (selObj.anchorNode.wholeText returns Click to add link, selObj.anchorOffset returns 0) - contentEditable should be set before the AddLink( ) function is called, and therefore you'll be starting with an editable second paragraph, with these browsers.

(*Although the selection is collapsed, the second paragraph is still activated, and a second run through the AddLink( ) function will get the demo to work, but a demo that doesn't work on a first try isn't worth anyone's while, needless to say. Moreover, it's not strictly true that editability cannot be suppressed pre-execution for Firefox and Camino as it is possible to re-add the original selection range to the collapsed selection prior to the createLink command, but this is a can of worms that I would just as soon not open.)

But it is at least possible to statically set document.getElementById("p1").contentEditable to true selectively for Firefox and Camino, and thereby for non-Mozilla browsers avoid giving the p1 p element a contentEditable="true" attribute, as follows:

<button type="button" onclick="AddLink( );">Click to add link</button>
<script type="text/javascript">
if (window.globalStorage && window.postMessage) /* Flags Firefox 3+ - see this JavaScript Kit page. */
document.getElementById("p1").contentEditable = "true";

FYI: Firefox and Camino will not color the link text, give a pointer cursor over the link, or write the link URL to the browser window's status bar unless editability is turned off after the foreColor command.

Creating more than one link

Opera, Safari, and Chrome allow me to create more than one link in the second paragraph without incident.

Upon pre-activating the second paragraph, I can create a first link with Firefox and Camino straightforwardly, but creating two or more links with these browsers is problematic - here's a chronology of what happens:
(1) After a first link is created and colored, p1's editability is turned off.
(2) Upon trying to create a second link in p1, the user selection is collapsed as described above; however, our second run through AddLink( ) turns p1's editability back on.
(3) A second attempt to create a second link in p1 is successful; however, in this AddLink( ) run selObj.anchorNode.parentNode returns [object HTMLParagraphElement], i.e., the anchor element markup is not detected. As a result, the foreColor command is not executed for the second link, and p1's editability remains on.
(4) With p1 editable, the browser does not impart normal click and mouseover behavior to the second link and it subtracts this behavior from the first link; it also sometimes (not reproducibly) subtracts the #ff0033 color from the first link.
(Steps (3) and (4) repeat with attempts to create additional links.)

So we now have two 'crippled' links: they're underlined and they have the browser's default link color (#0000ee), but clicking them doesn't take us to their target documents, and mousing over them neither loads their href values into the status bar nor gives us a pointer cursor. However, control-clicking these links does pop up a context menu appropriate for a link
[Firefox's link context menu]
and whose Open Link in New Window and Open Link in New Tab commands work as advertised.


The div below holds a cross-browser version of the "Creating a Link" demo combining Microsoft's original code with some of the code given in this post. Regarding browser support, the demo should work with the most recent versions of MSIE, Firefox, Opera, and Safari, and also with Camino and Chrome. For MSIE and Opera, the foreColor command will not take effect until the new link is deselected. For Firefox and Camino, the demo is good for creating one link, but I make no promises beyond that.

<title>Microsoft's "Creating a Link" Demo, Cross-Browser Version</title>

Select any portion of the following blue text, such as "My favorite Web site". Click the button to turn the selected text into a link.

My favorite Web site is worth clicking on. Don't forget to check out my favorite music group!

In the following entry, we'll examine in greater detail the "custom dialog business" that I gave short shrift to in the "Your URL, comrade" section.


Comments: Post a Comment

<< Home

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)