Dreamweaver MX Extensions

Workshop #2: A Command That Uses String Access

In this workshop, you create another command that starts simple and adds complexity. The command you create here shows that accessing the DOM as a string can be used to great effect when object access is inappropriate. This includes any time the user may want to perform operations on objects that are only partially selected (such as only one word of a text chunk ) or when the editing task involves pulling selected text out of a document, performing edits on it, and putting it back in the document.

Sample Command: Converting Selected Text to Uppercase

Have you ever wanted to convert selected text to uppercase in Dreamweaver? It seems straightforward, and in a way it isbut only if you use string access.

A little explanation is in order here. The goal of this command is to take exactly the text the user has selected and convert it to uppercase. If you refer back to the discussion of nodes and node types in Chapter 4 (see the section on "DOM Basics"), you'll remember that each chunk of text in a document constitutes an object of the TEXT_NODE type. So one way to accomplish this task would be to access the text object (using dom.getSelectedNode() ), access the text within the object by accessing its data property, and apply the JavaScript toUpperCase() string method, like this:

function ChangeCase() { myDOM = dw.getDocumentDOM(); myObject = myDOM.getSelectedNode(); myText = myObject.data; myObject.data = myText.toUpperCase(); }

The limitation of using this approach, however, is that it assumes an entire text object (with no surrounding objects) has been selected. Lifeand user selections are seldom that simple. For example, here are a few samples of what users might have selected in the document at the time they choose this command (in each case, the user's selection is shown in bold):

  • <p>It was a dark and stormy night.</p> . A simple selection consisting of text only, which is part of a larger text node object. You need to access the selection offsets, compare them to the node offsets, and create a substring of the node data.

  • <p>It was a dark and stormy night.</p> . The selection is an element node containing text. You need to determine that the selected object is not text, but has a text node as a child.

  • <p><font size ="2">It was a dark and stormy night.</font></p> . The selection is an element node containing an element node that contains a text node. This involves climbing down through two generations of childNodes.

  • <p>It was a <b>dark</b> and stormy night.</p> . The selection is an element node containing two text nodes and another element node, which then contains another text node. This involves some fancy maneuvering, sorting out childNodes.

  • <p>It was a <b> dark</b> and stormy night.</p> . The selection is a text node and part of an element nodethis can happen when the user double-clicks to select a single word. This selection is very difficult to isolate.

Can you see that trying to determine the selection by accessing nodes is going to get very complicated very quickly? A much more straightforward approach is to assume that the entire HTML code for the user's document is one large string, and then use the Dreamweaver dom.getSelection() method, along with the JavaScript substring method, to isolate exactly the text to be changed.

Task 1: Create the command file document

In your text editor, create a new document containing the command file framework (use the code from Listing 5.1 to start with). Save it in the Commands folder as Make Uppercase.htm .

Task 2: Create the basic code for the main function

Here's the idea: You access the entire HTML string ( outerHTML ) and assign it to a variable. Then access the selection as character offsets into the HTML string ( dom.getSelection() ), which means you can now use the JavaScript substring method on the HTML string to gain access to the individual characters in the selection. Using the substring method, divide the HTML string into three smaller strings: all the code before the selection, all the code in the selection, and all the code after the selection. Convert the selected string into uppercase; using the JavaScript toUpperCase() method, then join the three substrings back together using concatenation, and feed them back into the document as the new HTML string. Ready?

  1. Start by setting up your command file's main function. Change the name of the generic runCommand() function to a more appropriate name, like changeCase() . Then fill in comment lines indicating what the command needs to do (new code is in bold):

    function changeCase() { //gain access to the HTML element node object //collect the document's entire code as a string //collect the character offsets of the current selection //break the document string into three substrings //change the substring representing the selection to upper case //concatenate the three substrings back into one big string //replace the document's HTML string with the new string }

  2. Next , add the code that will access the document as an HTML string (new code is in bold):

    function changeCase() { //gain access to the HTML element node object var myDOM = dw.getDocumentDOM(); var myHTML = myDOM.documentElement; //collect the document's entire code as a string var myHTMLstring = myHTML.outerHTML; //collect the character offsets of the current selection //break the document string into three substrings //change the substring representing the selection to upper case //concatenate the three substrings back into one big string //replace the document's HTML string with the new string }

  3. Using the dom.getSelection() method, find the first and last character of the user's selection, and use that information to collect the three substrings ( beforeSelection, mySelection, afterSelection ):

    function changeCase() { //gain access to the HTML element node object var myDOM = dw.getDocumentDOM(); var myHTML = myDOM.documentElement; //collect the document's entire code as a string var myHTMLstring = myHTML.outerHTML; //collect the character offsets of the current selection var myOffsets = myDOM.getSelection(); //break the document string into three substrings var beforeSelection = myHTMLstring.substring(0,myOffsets[0]); var mySelection = myHTMLstring.substring(myOffsets[0],myOffsets[1]); var afterSelection = myHTMLstring.substring(myOffsets[1],myHTMLstring.length); //change the substring representing the selection to upper case //concatenate the three substrings back into one big string //replace the document's HTML string with the new string }

  4. So far, you're doing a lot of typing without seeing any results. As a diagnostic procedure, try adding a few window.alert statements to the end of your function code, which will tell you what's being collected in your three variables :

    //change the substring representing the selection to upper case //concatenate the three substrings back into one big string //replace the document's HTML string with the new string //examine the contents of the variables window.alert(beforeSelection); window.alert(mySelection); window.alert(afterSelection); }

  5. After you've added this last bit of code, try out your extension. Reload Dreamweaver extensions and open a document within Dreamweaver that contains various page elements you can select. Select something and run your command. Three alert windows should appear, one after the other, displaying all the HTML code up to your selected code, your selected code, and all the code after your selection. Figure 5.19 shows a typical result.

    Figure 5.19. Running the Make Upper Case command with three window.alert() statements to display different portions of the HTML code string.

    note

    The window.alert() statement was used throughout the Practice Session in the previous chapter as a quick way of getting feedback from Dreamweaver about DOM access. As you can see here, it's also a simple way to test the progress of variable assignments and calculations as you're developing projects.

  6. Now that you have isolated the selection and run a test to see that the code string is being properly subdivided, all you need to do is convert the text in the mySelection variable to uppercase and then concatenate the three substrings back together.

    To accomplish this, remove the window.alert() statements and change the end of your function to look like this (new code is in bold):

    function changeCase() { //gain access to the HTML element node object var myDOM = dw.getDocumentDOM(); var myHTML = myDOM.documentElement; //collect the document's entire code as a string var myHTMLstring = myHTML.outerHTML; //collect the character offsets of the current selection var myOffsets = myDOM.getSelection(); //break the document string into three substrings var beforeSelection = myHTMLstring.substring(0,myOffsets[0]); var mySelection = myHTMLstring.substring(myOffsets[0],myOffsets[1]); var afterSelection = myHTMLstring.substring(myOffsets[1],myHTMLstring.length); //change the substring representing the selection to upper case var newSelection = mySelection.toUpperCase(); //concatenate the three substrings back into one big string var newHTMLstring = beforeSelection + newSelection + afterSelection; //replace the document's HTML string with the new string myHTML.outerHTML = newHTMLstring; }

    Take this code for a test drive. Can you see any problems with it? Are there any situations where it just won't work, or where it does something you don't want it to? As written, the script has the following potential problems: The command can't distinguish between selected text and selected tags, so that if your selection includes elements other than text, all the information in those elements (tag names , attributes) is changed to uppercase. Or, the command also can't distinguish special characters (entities) such as &nbsp; , and converts those to uppercase. These problems are addressed next.

Task 3: Add the canAcceptCommand() function

Some of the problems you may be experiencing after completing Task 2 can be dealt with by making the command unavailable if there is no open document or if the selection isn't appropriate. This means adding a canAcceptCommand() function to your command file.

  1. Start with a very simple solution, adding this code to your command file's <script> tag:

    function canAcceptCommand() { var myDOM = dw.getDocumentDOM(); if (!myDOM) return false; var myOffsets = myDOM.getSelection(); var myObject = myDOM.getSelectedNode(); if (myOffsets[0] < myOffsets[1] && myObject.nodeType == "3") { return true; } else { return false; } }

    Can you see what's happening here? After gaining DOM access in line 2, line 3 gains access to the beginning and ending position of the selection in the document's HTML string. Line 4 gains access to the object (node) that contains the user's current selection. In line 5, a conditional statement tests for two conditions: If the two numbers stored in myOffsets are not the same number, then the beginning of the selection is not the same as the ending of the selection, which means there is actually something selected in the document. (Remember from Table 4.12 in the previous chapter that if the two offset numbers are the same, this means the selection is an insertion point.) That's the first condition that must be met. Then, if the object is of nodeType 3, which is the type for text objects, the selection must be part of a text object and therefore must consist of text. If both conditions are true, the function returns true , and the Make Uppercase command will be made available in the Command menu. If not, the function returns false , and the command will show as grayed out.

  2. This is a very strict standard, though, that doesn't allow any selections that include anything other than text. To see how limiting it is, create a practice document in Dreamweaver that contains the following lines of HTML:

    <p>It was a dark and stormy night.</p> <p>It was a <b>dark</b> and stormy night.</p>

    Triple-click in the first paragraph to select it. Then try to choose the Make Uppercase command. The command will be grayed out in the menu because the selection includes the entire <p> element, which is technically not an object of the text node type. (It's an object of the element node type).

    Now double-click the word dark in the second paragraph and try to choose the command. Again, double-clicking also selects part of the <b> element, which is not a text object.

    Finally, drag across portions of both paragraphs to select them and then try to choose the command. It will still be grayed out because in this case your selection includes more than one text object.

    You probably don't want your canAcceptCommand() function to be this strict.

  3. A better criterion is to make the command unavailable only if the selection contains absolutely no text. The code for this is substantially harder to write because you need to start from the user's selection, and then navigate as far as necessary down the DOM hierarchy until you either find text or hit bottom.

    Start by adding the following codewhich includes a revised canAcceptCommand() function and two new itemsto your document's <script> tag:

    var gTesting = 0; function canAcceptCommand() { var myDOM = dw.getDocumentDOM(); if (!myDOM) return false; var myOffsets = myDOM.getSelection(); var myObject = myDOM.getSelectedNode(); if (myOffsets[0] < myOffsets[1] && ( myObject.nodeType == "3" testGeneration(myObject))) { return true; } else { return false; } } function testGeneration(thisGen) { var myChildren = thisGen.childNodes; for (var a=0;a<myChildren.length;a++) { thisChild = myChildren.item(a); if (thisChild.nodeType == 3) { gTesting = 1; break; } if (thisChild.hasChildNodes()) { testGeneration(thisChild); } } return gTesting; }

    What does this code do? First, it declares a global variable, gTesting , that all functions in the command file will be able to access. This variable will eventually be assigned a value of 1 (for true ) if a text object has been found, or a (for false ) if no text object has been found. As a global variable, gTesting is automatically established as soon as the command file loads.

    The canAcceptCommand() function has been rewritten so that its test of acceptability is more complex. As before, the selection must be more than an insertion point ( myOffsets[1] must be a larger number than myOffsets[0] .) In addition to this, one of two conditions must be met:

    • The selection must be contained within a text object (of node type 3); or

    • If the selection is not contained within a text object, its containing object must be passed to a function called testGeneration() , which will determine if it any text elements are present.

    What does the testGeneration() function do? It takes an object (such as a <p> tag or a text chunk) as a parameter. In line 2 of the function, it gains access to all of that object's child objects. Starting in line 3, it uses a for loop to examine each child object in turn . For each child, the conditional statement in line 5 asks whether the object is a text object; if so, the gTesting global variable is set to 1 , and the loop ends. (There's no need to keep looking, because the goal of this whole set of functions is to determine whether even one text object exists.) If the child object is not a text object, a second conditional in line 9 asks whether the child object has any children of its own; and if so, the testGeneration() function calls another incarnation of itself to examine the child object.

    Calling itself like this makes testGeneration() recursive. It hunts through all sibling nodes and calls itself for each of these nodes that has child nodes. If it finds a text node, it stops (breaks). If it doesn't find a text node but finds child nodes, it calls itself to test each of those children. When it runs out of generations, it stops.

    When all the different instances of testGeneration() have finished running, the global variable gTesting is passed back to the canAcceptCommand() function. If any text element has been found, the gTesting variable will have been set to 1 , which canAcceptCommand() will interpret this as true . If no text elements have been found, the gTesting variable will still be set to its initial value of , which canAcceptCommand() will interpret as false .

Task 4: Refine the command to skip over entities and tag elements

Unless you were very strict in implementing canAcceptCommand() , you still have the challenge of telling the command to ignore tags and entities. One way to deal with this is to use a for loop to examine and change one special character at a time; when a < or & is found, skip all subsequent characters until finding > or ; . Can you see how this works? If a < character is found, it marks the beginning of an HTML tag. The script should ignore all characters until the end of the tag is indicated by finding a > character. Using the same logic, when a & is found, that indicates the beginning of an HTML entity such as &nbsp ; or &#147; . On seeing this character, the script should ignore all characters until the end of the entity is indicated by finding a ; character.

  1. To implement this refinement, start by changing the changeCase() function so that it processes one character at a time instead of the entire mySelection substring at once (new code is in bold):

    function changeCase() { [etc] //change the substring representing the selection to upper case var newSelection=""; var thisLetter=""; for (var a=0;a<mySelection.length;a++) { thisLetter=mySelection.charAt(a); thisLetter=thisLetter.toUpperCase(); newSelection+=thisLetter; } [etc] }

    In the previous version of the command, you changed the case of all the characters in mySelection and put them into newSelection all at once. In this version, you're creating a variable called thisLetter and adding one character at a time to it. Then you change the case of thisLetter tack its contents onto the existing contents of newSelection .

  2. To see how you'll proceed from here, add some comment lines to your code. Also add two variables (you'll see what they do by examining the comment lines):

    function changeCase() { [etc] //change the substring representing the selection to upper case var newSelection=""; var thisLetter=""; var toggle=1; var endChar; for (var a=0;a<mySelection.length;a++) { thisLetter=mySelection.charAt(a); //if thisLetter is < then turn toggle to 0 and turn endChar to > //if thisLetter is & then turn toggle to 0 and turn endChar to ; //if thisLetter is endChar then turn toggle to 1 //if toggle is 1 then change to upper case thisLetter=thisLetter.toUpperCase(); newSelection+=thisLetter; } [etc] }

    Can you see how the logic you're framing will work? The toggle variable will always have a value of 1 (true) or (false). Capitalization of the current character will take place only if the toggle is set to 1 . For every character the for loop encounters, if the character is the beginning of a tag ( < ) or entity ( & ), the toggle will be turned off (set to ), and the endChar variable will be set to the appropriate ending character for that element ( > or ; ). If the loop encounters the character specified by endChar , the toggle will be set back to 1 and capitalization will resume.

  3. Now all you need to do is add the code that will make each of these comments happen. It's all done with a series of simple conditionals. Add the following code to your function (new code is in bold):

    function changeCase() { [etc] //change the substring representing the selection to upper case var newSelection=""; var thisLetter=""; var toggle=1; var endChar; for (var a=0;a<mySelection.length;a++) { thisLetter=mySelection.charAt(a); //if thisLetter is < then turn toggle to 0 and turn endChar to > if (thisLetter=="<") { toggle=0; endChar=">"; } //if thisLetter is & then turn toggle to 0 and turn endChar to ; if (thisLetter=="&") { toggle=0; endChar=";"; ; //if thisLetter is endChar then turn toggle to 1 if (thisletter==endChar) { toggle=1; } //if toggle is 1 then change to upper case if (toggle==1) { thisLetter=thisLetter.toUpperCase(); } newSelection+=thisLetter; } [etc] }

    That's it! Try your command! You should be able to select all sorts of different text chunks and make them upper case. And you did it all by accessing the HTML code of the document as a string. Listing 5.2 shows the complete code for the command.

Listing 5.2 The Finished Code for the Make Upper Case Command, Commented for Reference

<html> <head> <title>Make Upper Case</title> <script language="JavaScript"> //to be used by canAcceptCommand() and testGeneration() var gTesting = 0; //determines if the command will be grayed-out in the menu function canAcceptCommand() { //gain access to object containing user's selection var myDOM = dw.getDocumentDOM(); if (!myDOM) return false; var myOffsets = myDOM.getSelection(); var myObject = myDOM.getSelectedNode(); //if user has text object selected, or selection contains a text object, return true if (myOffsets[0] < myOffsets[1] && myObject.nodeType == 3 testGeneration(myObject)) { return true; } else { return false; } } //tests the children of an object to see if they're text objects function testGeneration(thisGen) { //gain access to children of current object var myChildren = thisGen.childNodes; //test each child to see if it's a text object for (var a=0;a<myChildren.length;a++) { thisChild = myChildren.item(a); if (thisChild.nodeType == 3) { gTesting = 1; break; } //if the child has children, run another instance of this function to test them if (thisChild.hasChildNodes()) { testGeneration(thisChild); } } //if any child is a text object, this variable will be 1 return gTesting; } //main function of command function changeCase() { //gain access to the object created by the <html> tag and its contents var myDOM = dw.getDocumentDOM(); var myHTML = myDOM.documentElement; //collect the document's entire code as a string var myHTMLstring = myHTML.outerHTML; //collect the character offsets of the current selection var myOffsets = myDOM.getSelection(); //break the document string into three substrings var beforeSelection = myHTMLstring.substring(0,myOffsets[0]); var mySelection = myHTMLstring.substring(myOffsets[0],myOffsets[1]); var afterSelection = myHTMLstring.substring(myOffsets[1],myHTMLstring.length); //change the substring representing the selection to upper case var newSelection=""; var thisLetter=""; var toggle=1; var endChar; //step through each character in the selection for (var a=0;a<mySelection.length;a++) { thisLetter=mySelection.charAt(a); //if thisLetter is < then turn toggle to 0 and turn endChar to > if (thisLetter=="<") { toggle=0; endChar=">"; } //if thisLetter is & then turn toggle to 0 and turn endChar to if (thisLetter=="&") { toggle=0; endChar=";"; } //if thisLetter is endChar then turn toggle to 1 if (thisLetter==endChar) { toggle=1; } //if toggle is 1, set thisLetter to upper case if (toggle==1) { thisLetter=thisLetter.toUpperCase(); } newSelection+=thisLetter; } //concatenate the three substrings back into one big string var newHTMLstring = beforeSelection + newSelection + afterSelection; //replace the document's HTML string with the new string myHTML.outerHTML = newHTMLstring; } </script> </head> <body onLoad="changeCase()"> </body> </html>

Категории