Skip to content

Instantly share code, notes, and snippets.

@salsalabs
Last active March 12, 2021 19:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save salsalabs/fa7cdd9d27f955bdc10e9cbac522cec5 to your computer and use it in GitHub Desktop.
Save salsalabs/fa7cdd9d27f955bdc10e9cbac522cec5 to your computer and use it in GitHub Desktop.
Wait for an element to appear then modify it. This is the best method to wait for a field to appear in targeted actions and multi-content targeted actions.

Wait for an element and modify it

Background

From time to time, Salsa Classic clients want to change an element on a page. That's a snap when the element already appears on the page. However, fields that appear in targeted advocacy actions may not appear on the first displayed page.

Targeted advocacy actions in Salsa Classic are executed as a series of pages. The pages are loaded into the same HTML page using AJAX. The general flow of pages is

  1. Gather address and zip.
  2. Provide personal information and custimize the letter to the target(s).
  3. Submit the letter.
  4. (Optional) provide extra information that the targets need.

Note that each of the pages is loaded into the same location in the targeted action. Pages appear exclusively and can't be modified after they've been replaced.

Challenge

The challenge is, in a lot of cases, that Salsa Classic clients would like to modify the content of the Letter page. These changes, are typically

  • Hide the target's contact information (phone and/or email),
  • Change the appearance of one or more fields on the Letter page, or
  • Add a messages or some text around input fields.

A standard script won't work for these purposes. Scripts run when the page is loaded into the browser. The Letter page hasn't arrived when the page is showing. A script in the page HTML will run but won't actually change anything.

Solution

The solution is to have a script wait for a field to appear. For example, wait for the Legislator contact information so that it can be hidden. We've cooked up an way to wait for a field without using up a lot of browser resources.

TL;DR The solution looks for the field to be modified. If it's already part of the page, there the field is modified immediately and the job is done. If the field does not appear, then the solution uses a MutationObserver to wait for the field of your chose to appear.

The workhorse of this solution is the waitForElement() method on this page.

Before you begin

  1. Determine the selector for the root element that needs to be watched. For example, the selector for the root element in targeted actions is #mainForm.
  2. Determine the selector for the element that you're waiting for. Some examples:
    • Cell_Phone: "input[name=Cell_Phone]"
    • Contact information: "div.recipient .number"
    • State: "select[name=State]"
  3. Write a function named modify() that will modify the element once it's found. The modify() function will be called for each of the elements that match the provided element selector.

Installation

Important note

You only need to install the waitForElement.html file one time, even if you are using waitForElement() to make several modifications on more than one page. Insert the modify() function as shown in waitForElement.html.

If you do have more than one modify() on a page, then

  1. Rename the modify() functions to reflect what is being modified. For example, modifyPhoneNumber().
  2. Add a waitForElement() call that is configured for your modify function. See the examples (below).
  3. Test, test, test!

Steps you can use

Prepare the template
  1. Edit a template used by actions.
  2. Use the browser's search tool to find the </body> tag.
  3. Insert a blank line before the </body> tag.
Copy the element waiter
  1. Click here to go to the waiter source.
  2. Locate and click the "Raw" button. A new window will appear that contains just the solution.
  3. Copy the contents of the window and close it.
  4. Return to the template editor.
  5. Paste the copied text into the newly inserted blank line.
Copy the modify function
  1. Click on one of these links to view the modify function.
  2. Locate and click the "Raw" button . A new window will appear that contains just the solution.
  3. Copy the contents of the window and close it.
  4. Return to the template editor.
  5. Locate the marker that shows where to put the modify function.
/* -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    PUT YOUR modify() FUNCTION HERE.
    PUT YOUR waitForElement() CALL HERE.
   -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */
  1. Replace the marker with the text copied from the README file.
Finish up
  1. Save the template.
  2. Test!

Extras

The script to change the content of the "District:" line contains a special version of the wait handler. If you'd like to this script in your website, then

  1. Download the script from this page to your disk.
  2. Upload the script to your Salsa Classic instance. Click here if you need help with that.
  3. Uploading the script will result in a URL for the uploaded file. Copy that.
  4. Choose the template used by your Advocacy actions.
  5. Insert this text just before the tag. Replace [[the script's URL]] with the URL copied in step 3.
 <!-- BEGIN Change "District" line on the contact page for actions. -->
    <script type="text/javascript" src="[[the script's URL]]"></script>
 <!-- END   Change "District" line on the contact page for actions. -->

Questions?

If you have any questions, then please collect this information and send it to support@salsalabs.com. We'll be glad to help.

  • Your organization KEY
  • Your chapter KEY if you are in a chapter of another organization
  • The template_KEY of the modified
  • The URL of an action that uses the template.
  • A screenshot of the action showing the issue (if possible)

Send this information to support@salsalabs.com. We'll be glad to help.

<!-- BEGIN Add a text line below the Cell_Phone field. -->
<script type="text/javascript">
(() => {
// Script to modify the Cell_Phone by adding text underneath it.
// The text to add appears below in a hidden <div> tag.
// @param [Node] e <input> for Cell_Phone
modifyCellPhone = (e) => {
e.parentNode.appendChild(document.querySelector("#cell-phone-message"));
}
// Wait for Cell_Phone to appear and modify it. You can read this as
// "Wait for input[name=Cell_Phone] to appear in #mainForm and modify it."
document.addEventListener("DOMContentLoaded", () => {
waitForElement("#mainForm", "input[name=Cell_Phone]", modifyCelPhone);
})
})();
</script>
<div style="display: none;">
<!-- Text that's inserted below the Cell_Phone. -->
<p id="cell-phone-message">This is a message under the Cell Phone field.</p>
<style type="text/css">
/* Sample CSS for the cell phone message. Change this to fit your needs. */
#cell-phone-message {
font-size: 12pt;
font-style: italic;
color: darkgray;
margin-bottom: 10px;
}
</style>
</div>
<!-- END Add a text line below the Cell_Phone field. -->
// Script to wait for the letter page of an action and change the contents of
// the "District:" line. If a state legislator has a named district (a la Massachusetts),
// then that replaces the text after "District:". In all other cases, the text
// "District:" is removed.
(() => {
// Wait for the letter page. That's where the legislators appear.
// @param [String] target selector for the root element (for example, "#mainTarget")
// @param [String] selector selector for element that we're waiting for (for example, "input[name=Cell_Phone]'
// @param [Function] modify call this method when the element is found
// @see https://gist.github.com/salsalabs/fa7cdd9d27f955bdc10e9cbac522cec5
window.waitForLegislators = (target, selector, modify) => {
// If this is not a *targeted* action, then the element that we're looking for is
// already on the page -- no need to wait for it.
var e = document.querySelector(target);
if (e !== null) {
e = e.querySelector(selector);
if (e !== null) {
modify(e);
return;
}
}
var done = false;
var observer = null;
// Wait for the element identified by "selector"
var callback = (mutations) => {
if (done) {
// Skip execution if we're already finished...
return;
}
mutations.forEach(function (mutation) {
if (done) { return; }
if (mutation.type == "childList") {
// @see https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord
var list = document.querySelector(target).querySelectorAll(selector);
if (list.length != 0) {
// Changes will trigger more mutations. Turn off the observer
// now to avoid an infinite loop.
observer.disconnect();
Array.from(list).forEach(modify);
}
}
});
};
var config = {
childList: true,
subtree: true
};
observer = new MutationObserver(callback);
observer.observe(document.querySelector(target), config);
}
// Modify a single recpient.
// @param db Object Legislative database
// @param r Object Element containing the recipient
var handleRecipient = (db, r) => {
var e = r.parentNode.parentNode.querySelector('input[name="targetId"]')
if (e == null) { return }
var targetId = e.value
var legislator = db[targetId];
var districtType = legislator.targetTypeCode;
var districtName = null;
switch (districtType) {
case "SH": districtName = legislator.district;
case "SS": districtName = legislator.district;
default: break;
}
var dt = r.querySelector("div.district")
if (dt != null) {
if (districtName == null) {
var h = dt.innerHTML.replace("District:", "")
dt.innerHTML = h;
} else {
var b = dt.querySelector('b')
b.innerHTML = districtName
}
}
}
// Script to modify legislators "District" contents.
//
// If the legislator has a specific district name (e.g. Mass legislative
// district), then the content after "District" is filled with the district
// name. If the legislator does not have a specific district name, "District"
// is removed. That will leave the legislative title.
//
// @param e Object Required parameter. Ignored in this function.
var modifyDistrictInfo = (e) => {
var x = document.querySelector('input[name="presenceJSON"]');
if (x == null) { return }
var j = JSON.parse(x.value)
// Convert to an object to avoid linear search for each legislator.
//if (db == null) {
var db = j.targets.reduce((a, target) => {
var key = target.targetId
a[key] = target;
return a;
}, {})
// }
// Handle a single recipient.
handleRecipient(db, e)
}
// Wait for the contact information to appear, then work on the
// "District" info.
document.addEventListener("DOMContentLoaded", () => {
if (document.location.href.indexOf("action4/common/public") != -1) {
waitForLegislators("#mainForm", "div.recipient", modifyDistrictInfo);
}
})
})();
<!-- BEGIN Hide contact information for displayed targets in targeted action. -->
<script type="text/javascript">
(() => {
// Script to hide an element. Called to hide Phones and Faxes for targets.
// @param [Node] e <div> tag that needs to be hidden.
modifyConnectionInfo = (e) => {
e.style.display = "none";
}
// Wait for the contact information to appear, then hide it. Notice that *all*
// contact information on the Letter page will be hidden. Neato, keeno!
document.addEventListener("DOMContentLoaded", () => {
waitForElement("#mainForm", "div.number", modifyConnectionInfo);
})
})();
</script>
<!-- END Hide contact information for displayed targets in targeted action. -->
// Make the entries in the LTE newspaper grid more attractive. We'll accomplish that by
// using a mutation observer (`waitForElement()`) to wait for the newspaper grid to appear.
// Once it appears, then we'll clean up each newspaper in the grid using `modifyPaper`.
// The first cleanup is to replace the URL with a link containing the URL.
//
// Unlike other solutions on this page, `waitForEvent` is embedded in this file.
(() => {
// Task starts here. Wait for the newspaper grid to appear,
// then call modifyPaper to change it.
document.addEventListener("DOMContentLoaded", () => {
var isLetter = RegExp("letter/.*letter_KEY=\\d+").test(window.location.href);
if (isLetter) {
waitForElement("#salsa", "div.address", modifyPaper);
}
})
// If this is an LTE and there are papers then convert the URL to a link.
// Note: This is the "modify" function for waitForElement.
const modifyPaper = (e) => {
Array.from(document.querySelectorAll("div.address"))
.filter((e) => {
return e.querySelector("a") == null
})
.forEach((e) => {
var t = e.innerHTML
var a = e.innerHTML.split("\t").join("").split("\n").join("").split("<br>")
a[2] = `<a href="${a[2]}" target="_blank">Link</a>`
e.innerHTML = a.join("<br>")
})
}
// Function to wait for an element, then cause the element to be
// modified when it appears.
// @see https://gist.github.com/salsalabs/fa7cdd9d27f955bdc10e9cbac522cec5
// @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
// @see https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord
window.waitForElement = (target, selector, modify) => {
var e = document.querySelector(target);
if (e !== null) {
e = e.querySelector(selector);
if (e !== null) {
modify(e);
return;
}
}
var done = false;
// Wait for the element identified by "selector"
var callback = function (mutations) {
// if (done) {
// // Skip execution if we're already finished...
// return;
// }
mutations.forEach(function (mutation) {
if (mutation.type == "childList") {
var list = document.querySelector(target).querySelectorAll(selector);
if (list.length != 0) {
Array.from(list).forEach(modify);
done = true;
}
}
});
};
var observer = new MutationObserver(callback);
var config = {
childList: true,
subtree: true
};
observer.observe(document.querySelector(target), config);
}
})()
<!-- BEGIN Default the country code to "US". -->
<script type="text/javascript">
(() => {
// Script to change the country code.
// @param [Node] e <div> tag that needs to be hidden.
modifyCountry = (e) => {
if (e.value === null || e.value.length === 0) {
e.value = "US";
}
};
// Wait for the country code to appear, then modify it.
document.addEventListener("DOMContentLoaded", () => {
waitForElement("#salsa form", "*[name=Country]", modifyCpuntry);
})
})();
</script>
<script type="text/javascript">
(() => {
// Function to change an element on a page.
//
// If the current page is not a targeted action, then the desired element already appears on
// the page and can be modified immediately. If the current page *is* a targeted action, then
// we'll wait for the element to appear before modifying it.
// @param [String] target selector for the root element (for example, "#mainTarget")
// @param [String] selector selector for element that we're waiting for (for example, "input[name=Cell_Phone]'
// @param [Function] modify call this method when the element is found
window.waitForElement = (target, selector, modify) => {
// If this is not a targeted action, then the element that we're looking for is
// already on the page -- no need to wait for it.
var e = document.querySelector(target);
if (e !== null) {
e = e.querySelector(selector);
if (e !== null) {
modify(e);
return;
}
}
var done = false;
// Wait for the element identified by "selector"
var callback = function(mutations) {
if (done) {
// Skip execution if we're already finished...
return;
}
mutations.forEach(function(mutation) {
if (mutation.type == "childList") {
// @see https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord
var list = document.querySelector(target).querySelectorAll(selector);
if (list.length != 0) {
Array.from(list).forEach(modify);
done = true;
}
}
});
};
var observer = new MutationObserver(callback);
var config = {
childList: true,
subtree: true
};
observer.observe(document.querySelector(target), config);
}
/* -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
PUT YOUR modify() FUNCTION HERE.
PUT YOUR waitForElement() CALL HERE.
-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */
})();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment