Buttons That Add And Delete PDF Pages
Sometimes there's not enough room on a PDF form for the information that needs to be captured.
In fact, the form might require multiple copies of the same page, depending on the situation. The user might only need one page, or two, or any number of copies of the same page. To avoid creating a PDF form that displays multiple pages that might never be used, because they are copies of the same page, buttons for adding and deleting pages can be placed on the form. Additional pages are only displayed when needed. Doing this properly presents some challenges.
Challenges
1) As I wrote about in my post Check Boxes That Show And Hide PDF Pages, you can't show and hide template pages with Adobe Reader so the pages must be spawned to accommodate end users without access to Acrobat Pro. Reader can only delete pages that were spawned in the current session – another reason spawning is the solution.
2) When spawning template pages, there is an option of whether or not to rename the fields. If the purpose of the form is to capture additional information with each spawned page, the fields must be renamed, because multiple fields with the same name can only have one value. If you change the value in one field it will automatically change in all fields with the same name.
3) In this example the template page contains math calculations that need to work separately on every page created during spawning. Because the fields will be renamed, the calculations must anticipate the renamed field names and adjust for them, so that they work on every spawned page, no matter how many pages are spawned. In my article Anticipating Field Names In Custom Calculation Scripts, I described in detail how to extract the number suffix from an anticipated field name when the Right-Click > Place Multiple Fields function is used, then add the suffix back into the field names so the calculation works on each additional field without having to modify the script. For template pages spawned with renamed fields, a prefix is added to the field names. I will demonstrate how to anticipate the new field names, extract the prefix from the field names, and add it back into the field names in the calculation script so it works on every spawned page, without having to modify the script.
4) As pages are added and deleted using the buttons, the page numbers need to adjust so they are always correct. Since the page numbers are field values, they must be prevented from clearing their values if the form contains a reset button.
5) This project will start as a one-page document with an Add Page button only (no Delete Page button). The Add Page button will only be available on the last page that is spawned so that additional pages are added in order. The last spawned page will also contain a Delete Page button to force spawned pages to be deleted in the reverse order of which they were created. Once the Add Page button from the original page is activated it needs to be hidden so it can't be used again, unless all spawned pages are deleted, and only the original page is present again. When spawned pages are deleted, the Add Page button needs to appear on the last spawned page only (or the original page if that is the only one remaining).
The Add and Delete Page buttons would normally appear at the bottom of the pages, but I placed them in the top part of the page under the calculation so that scrolling is unnecessary when testing and demonstrating this project.
Video of Completed Project
Download The File To Program This Project
Take the Acrobat Pro/JavaScript eLearning Course Acrobat Like a Pro
The Project, Step by Step
1) The first page will be programmed first, then the second page. The second page will then be made into a template and hidden. A document level script function that is called in the delete page buttons will be added.
2) First, the TOTAL field is a total of fields Subtotal 1 through Subtotal 4. For this simple addition calculation, I would usually use Value is the sum of feature in the calculation tab of the field properties, and not a custom calculation script. In this case however, I wrote a custom calculation script that extracts the prefix of the TOTAL field name (if any) and adds it to the beginning of the field names for Subtotal 1 through Subtotal 4. It also loops through these fields and adds their values together. If the fields don't have a prefix, it will extract nothing and add nothing to the beginning of the other field names, so the script will still work. To extract the prefix without knowing exactly what it will be (but knowing its structure), the text string of the field name is split by the periods, and the prefix portion is reassembled. This custom calculation script is not necessary on the TOTAL field of the first page because the fields will be named differently on the template page. I'm using it on the first page for demonstration purposes. If you've read any of my previous scripting articles, you'll know that field naming is well thought out for simplicity of scripting. For example, the only change when copying and pasting the Total field to the template page is a one letter change from uppercase to lowercase. Copy the following script and paste it as a custom calculation in the TOTAL field of the first page.
var prfx=""; // Line 1
var ttl=0; // Line 2
var aray=event.target.name.split("."); // Line 3
if(aray.length>=3) // Line 4
{prfx=aray[0]+"."+aray[1]+"."} //Line 5
for(var i=1;i<5;i++) // Line 6
{ // Line 7
ttl+=Number(this.getField(prfx+"Subtotal "+i).value) // Line 8
} // Line 9
event.value=ttl; // Line 10
Line By Line Explanation of The Script
Line 1: Define the variable prfx as an empty string;
Line 2: Define the variable ttl as 0 (zero).
When a template page is spawned and the bRename parameter is set to true (that is, the fields will be renamed), we know the renaming structure of the fields. A prefix will be added to the field name. The prefix consists of the letter P, followed by a number* which represents the number of times the page was spawned, followed by a period, followed by the template name, followed by another period.
The reason the pages must be deleted in reverse order is to avoid duplication of field names. For example, if the page is spawned four times “a number*” referenced in the previous paragraph will be one through four. If any of those spawned pages, except the last one containing the number four, are deleted and the template page is spawned again, there will be a duplication of fields. That is, only three spawned pages will remain so the “a number” will be four, but the four number already exists in field names, causing duplication.
If a template named myPage, containing a field named myField, was spawned for the first time, myField would be renamed P1.myPage.myField. All we really need to know is the new field name will be the old field name (myField) preceded by two periods with text before and after the first period. To extract this "prefix" we can split the file name by the periods and reassemble the prefix portion.
Line 3: Create an array and define it with the variable aray by splitting the field name by periods. Because event.target is used as the field name in scripts that call their own field name, instead of this.getField(<field name>), we can use this script in any field without knowing its actual field name.
Line 4: If statement: If aray.length is greater than or equal to 3. In other words, if the field name contains 2 or more periods.
Line 5: If the statement on the previous line returns true, the prfx variable is index 0 of the array, followed by a period, followed by the index 1 of the array, followed by another period.
Line 6: The first line of a for loop, starting with the variable i is equal to 1, and ending when i is equal to 4 ("<5"). There are 4 fields that will be added together for the total. Adjust the 5 according to the number of fields. Example: 10 fields, change the 5 to 11.
Line 7: Opening curly brace to contain the operation inside the loop.
Line 8: Add the field values to the running total of the variable ttl. Notice the field name is the prfx variable, plus "Subtotal " (the file name without the number and a space), plus i (the number in the loop,1 through 4 to capture all Subtotal fields).
NOTE: The plus sign in JavaScript can be addition or concatenation. Notice this.getField(prfx+"Subtotal "+i).value is inside the parenthesis of the Number( ) constructor. This is to force the script to add the numbers together instead of concatenating them. If the JavaScript engine interprets a number as a string it will concatenate numbers. For example, 25 + 25 will equal 50 if the JS engine interprets the two 25s as numbers. But if it interprets either of them as a string, 25 + 25 will equal 2525 (concatenation). Since the Subtotal fields are formatted to accept only numbers, a regular addition calculation will only be interpreted as numbers. In this calculation, if a number constructor is not used and the user enters numbers in the field order, there will be no issue. However, if the user skips a field the blank field will be interpreted as an empty string in this calculation causing the numbers to concatenate (25 + empty string + 25 = 2525).
Line 9: Curly brace to end the containment of the loop operation.
Line 10: Set the value of the field to the variable ttl (the total of the all the Subtotal field values).
3) Test the calculation by entering numbers into the Subtotal fields and clear the Subtotal fields.
4) Copy the following script and paste it as a mouse up action in the Add Page button field:
this.getTemplate("Page 1").spawn();// Line 1
event.target.display=1; // Line 2
this.pageNum++;// Line 3
this.calculateNow();// Line 4
Line By Line Script Explanation
Line 1: Spawns the template named Page 1. The spawn method has four optional input parameters. Because the defaults provide exactly what we want they are all excluded. The default of the nPage parameter is 0 and the default of bOverlay parameter is true. Since the document only has one page at this point (when we hide the template), these settings will spawn the page after page 1. The default of the bRename parameter is true, which means the fields will be renamed when the template page is spawned.
Line 2: Sets the visibility of the Add Page button to hidden. (Yes, I know, the documentation says the script is event.target.display=display.hidden. My script works and requires less typing.)
Line 3: Advances the focus to the next page. Since the user just clicked Add Page, he probably wants to be on that page. In later versions of Adobe this line is not necessary because the focus goes to the spawned page. Not so in all versions.
Line 4: Forces the field calculations in the form to run. Whenever a field value changes, all field calculations in the form run. Because the page numbers on the spawned pages have calculations to set the page numbers, we need those calculations to run every time a page is spawned. This line of code forces those calculations to run without changing any field values.
5) In form editing mode (Ctrl + Shift + 7), copy the Subtotal and TOTAL fields, paste them on page 2, and align them.
JavaScript is case sensitive. I made a popular YouTube video How To Make A Dynamic PDF Stamp, and I get a lot of “I-followed-your-instructions-exactly-and-it-doesn’t-work” comments. If I can’t fix the issue after a couple of back-and-forth comments I will ask the commenter to contact me outside of YouTube. Nine times out of ten the script either has typos and/or case sensitivity issues. When writing scripts CASE SENSITIVITY MATTERS. In this project I use case sensitivity to my advantage by naming the template fields the same as the page 1 fields, but with lower case first letters instead of upper case. This makes the script easy to write and follow without having to think of unique field names for the template page.
6) Rename the pasted fields by changing all upper-case letters to lower-case letters. Even though these fields will be renamed in the spawning process, it is important that they don't share the page 1 field names. The reason is this: When the end user fills in the fields with values, before adding another page (spawning), those values will go into the template page fields even though the page is hidden, and they will remain in those fields when the page is spawned. When a page is added we don't want any values in its fields.
7) In the calculation tab of the total field (renamed from TOTAL in the previous step), make one change. Change the uppercase S in Subtotal of the following line to lower case:
ttl+=Number(this.getField(prfx+"Subtotal "+i).value)
After the change:
ttl+=Number(this.getField(prfx+"subtotal "+i).value)
8) Copy the following script as a custom calculation script in the PageNumber field at the bottom of the page:
event.value=event.target.page+1; //Line 1
event.target.defaultValue=event.value; // Line2
Script Explanation
Line 1: Set the value of the field to the page number property of the field, plus 1 (page numbers are zero-based so the first page is 0, the second page is 1, etc. This script will run when a page is spawned and this.calculateNow( ) is activated, as previously described on the script breakdown of the Add Page button.
Line 2: Set the default value of the field to the value of the field. When a field is reset during the resetForm( ) function, the field value reverts to its default value. This line of code will prevent the page numbers from clearing if those fields are reset.
9) Copy and paste the following script as a mouse up action the actions tab of the add page button:
this.getTemplate("Page 1").spawn(); //Line 1
event.target.display=1; // Line 2
this.getField(event.target.name.replace("add","delete")).display=1; //Line 3
this.pageNum++; //Line 4
this.calculateNow(); //Line 5
Script Explanation
Lines 1, 2, 4, and 5 are identical to lines 1 through 4 of the Add Page button on page 1 so the explanation won’t be repeated. Line 3 is new for this button.
Line 3: Sets the delete page button to hidden when another page is added. Because the delete page button will always have the same name as the add page button on the same page (except for the word "delete" in place of "add"), there is no need to split the field name and reassemble the prefix portion. Instead, "add" is replaced by "delete" in the add page button name to obtain the delete page button name in the renamed fields after spawning.
10) Copy and paste the following script as a document level script:
function showAddPage() // Line 1
{// Line 2
for(var i = this.numFields - 1; i > -1; i--)// Line 3
{// Line 4
var fieldName = this.getNthFieldName(i);// Line 5
if(/add page/.test(fieldName) && this.getField(fieldName).page==this.numPages-2) // Line 6
{// Line 7
this.getField(fieldName).display=2;// Line 8
}// Line 9
if(/delete page/.test(fieldName) && this.getField(fieldName).page==this.numPages-2)// Line 10
{// Line 11
this.getField(fieldName).display=2;// Line 12
}// Line 13
}// Line 14
}// Line 15
Line 1: Defines a function that will make visible the correct add page and delete page buttons when a delete page button is clicked. This function will be called in a mouse up action of the delete page buttons.
Line 2: Opening curly brace to contain the function.
Line 3: Start of a for loop that loops through all fields in the form.
Line 4: Opening curly brace to contain the loop functions.
Line 5: Defines the variable fieldName as the field name being operated on.
Line 6: If statement with two tests. The first test, /add page/.test(fieldName) tests the field name with a regular expression to verify if the field name contains the phrase add page. This covers all prefixed field names containing the phrase add page. The second test is whether the page property of the field (page number the field is on) is equal to the number of pages in the document, minus 2. This is actually the previous to one containing the button being clicked. Pages numbers are zero-based so the last page of the document is this.numPages-1, and the second-to-last page is this.numPages-2;
Line 7: Curly brace to contain what happens when the if statement in the previous line returns true.
Line 8: If the test on line 6 returns true (field name contains add page and field is on the second to last page), the visibility of the add field button is set to 2 (visible but doesn’t print).
Line 9: Curly brace to close the result of the if statement.
Lines 10 through 13: Same as lines 6 through 9, but using the delete page phrase and buttons instead of add page.
Line 14: Curly brace to close out the loop.
Line 15: Curly brace to close out the function.
11) Copy the following script and paste as a mouse up action in the delete page button field:
showAddPage(); // Line 1
this.deletePages(this.pageNum,this.pageNum); // Line 2
if(this.numPages==1) // Line 3
{this.getField("Add Page").display=2} // Line 4
Script Explanation
Line 1: Call the function entered as a document level script in step 10.
Line 2: Delete the current page.
Line 3: If statement to test whether there is only one page left in the document.
Line 4: If the previous line returns true, set the visibility of the Add Page button on page 1 to 2 (visible but doesn’t print).
12) Create a template for page 2 called "Page 1" (see video at the end).
13) Hide the template page.
14) Save the PDF.