Skip to main content
Parker Davis

eBird Compress, my first bookmarklet

Introduction

I only recently learned that bookmarklets exist. A portmanteau of bookmark and applet, they are pieces of Javascript you can run from the address bar or be called from your bookmarks. The idea of running Javascript directly from the address bar sounds strange at first but has fun applications, literally.

I previously built an alternative Rare Bird Alert! web application whose primary feature was grouping rare eBird sightings by species into expandable <details> elements by pulling data from the public eBird API. Creating the same feature for your eBird Needs Alerts is a little trickier because it requires authentication and access to your personal eBird data. Instead of some convoluted login and CSV data upload system, how about we just modify the DOM on the actual eBird website to group sightings by species?

Here it is

javascript:(()=>{const r=document.querySelectorAll(".Observation");if(r.length===0){alert("No eBird observations found on page!");return}if(document.querySelector("[data-species]")){alert("Already run script. If you are having issues, reload page and try again.");return}const c=r[0].parentNode;function i(t){return t.querySelector(".Observation-species .Heading-main").innerText}function u(t){let e=new Set;for(const n of t){const o=i(n);e.add(o)}return e}function p(t,e){for(const n of t){const o=document.createElement("details");o.setAttribute("data-species",n),o.setAttribute("style","margin-top: 0.5em");const s=document.createElement("summary");s.setAttribute("style","cursor: pointer; list-style:"),s.innerText=n,o.appendChild(s),e.appendChild(o)}}function d(t){for(let e of t){const n=i(e),o=document.querySelector(`[data-species="${n}"]`),s=e.previousElementSibling;o.appendChild(s),o.appendChild(e)}}function l(t){for(species of t){const e=document.querySelectorAll(`[data-species="${species}"] .Observation`).length.toString(),n=document.querySelector(`[data-species="${species}"] summary`);n.innerText=`${n.innerText} (${e})`}}const a=u(r);p(a,c),d(r),l(a)})();

How to use it

  1. Create a random bookmark (this blog post for instance)
  2. Open your bookmark manager
  3. Edit the bookmark you just created, rename it to whatever you want (maybe "eBird Compress"), and replace the URL with the code above
  4. Save your new bookmark

Now when you go to any eBird alert page you can open the bookmark that you just created and voila!

Demo

eBird compress demo

It works on both desktop and mobile. Consult your browser's documentation if you're not sure how to edit and save bookmarks.

So, what is the code doing?

Here is the full non-minified script for your perusal. If you are curious how it works, here is a beat by beat breakdown:

First, we store a NodeList of all the DOM elements for observations in a variable called observations. If no observations are found (like if the script is run on a non-eBird alert page) it shows an alert message and stops the execution of the script. If the script has already been run on a page, it will alert you about that as well.

const observations = document.querySelectorAll(".Observation");
if (observations.length === 0) {
	alert("No eBird observations found on page!");
	return;
}
if (document.querySelector("[data-species]")) {
	alert("Already run script. If you are having issues, reload page and try again.");
	return;
}

The parent container of the observations is stored in another variable. We define a function to extract the species name from an element, then a function to create a set of unique species names. Sets can only store unique values so no need to check for duplicate species names.

const parentContainer = observations[0].parentNode;

function getSpeciesName(observation) {
	return observation.querySelector(".Observation-species .Heading-main")
		.innerText;
}

function getUniqueSpeciesList(observations) {
	let uniqueSpecies = new Set();
	for (const observation of observations) {
		uniqueSpecies.add(getSpeciesName(observation));
	}
	return uniqueSpecies;
}

We define a function that will create new <details> DOM elements for each species, assign the species name to a data-species attribute and a <summary> element, then mount the new DOM elements where they need to go.

function createUniqueSpeciesWrappers(uniqueSpeciesSet, containerNode) {
	for (const species of uniqueSpeciesSet) {
		// Create wrapper node
		const detailsNode = document.createElement("details");
		// Add data-species attribute
		detailsNode.setAttribute("data-species", species);
		// Add spacing styles
		detailsNode.setAttribute("style", "margin-top: 0.5em;");
		// Create summary node
		const summaryNode = document.createElement("summary");
		// Style to visually indicate clickability
		summaryNode.setAttribute("style", "cursor: pointer;");
		// Insert species name into summary element
		summaryNode.innerText = species;
		// Add summary element to details
		detailsNode.appendChild(summaryNode);
		// Add details element to container
		containerNode.appendChild(detailsNode);
	}
}

We then define a function to move all of the observations into the new DOM elements we just created.

function moveObservationsIntoWrappers(observations) {
	for (let observation of observations) {
		const commonName = getSpeciesName(observation);
		const detailNode = document.querySelector(`[data-species="${commonName}"]`);
		// Also, grab the spacer node just before each observation
		const spacerNode = observation.previousElementSibling;
		detailNode.appendChild(spacerNode);
		detailNode.appendChild(observation);
	}
}

The spacer nodes are also grabbed and moved because otherwise you end up with a mess of gray bars at the top:

Spacer nodes shown jumbled at the top of the list

Next, we get the number of observations for each species and append that number to the name of each species.

function appendSpeciesObsCount(list) {
	for (species of list) {
		const observationCount = document
			.querySelectorAll(`[data-species="${species}"] .Observation`)
			.length.toString();
		const summaryNode = document.querySelector(`[data-species="${species}"] summary`);
		summaryNode.innerText = `${summaryNode.innerText} (${observationCount})`;
	}
}

All that is left to do is call all of the functions to action!

// GO GO GO!
const uniqueSpeciesList = getUniqueSpeciesList(observations);
createUniqueSpeciesWrappers(uniqueSpeciesList, parentContainer);
moveObservationsIntoWrappers(observations);
appendSpeciesObsCount(uniqueSpeciesList);

We then run the above code through esbuild's code minifier, which basically removes whitespace and shortens variable names to keep all the same functionality in a more compressed format. The result is terse code ready for a bookmark:

javascript: (()=>{const r=document.querySelectorAll(".Observation");if(r.length===0){alert("No eBird observations found on page!");return}if(document.querySelector("[data-species]")){alert("Already run script. If you are having issues, reload page and try again.");return}const c=r[0].parentNode;function i(t){return t.querySelector(".Observation-species .Heading-main").innerText}function u(t){let e=new Set;for(const n of t){const o=i(n);e.add(o)}return e}function p(t,e){for(const n of t){const o=document.createElement("details");o.setAttribute("data-species",n),o.setAttribute("style","margin-top: 0.5em");const s=document.createElement("summary");s.setAttribute("style","cursor: pointer; list-style:"),s.innerText=n,o.appendChild(s),e.appendChild(o)}}function d(t){for(let e of t){const n=i(e),o=document.querySelector(`[data-species="${n}"]`),s=e.previousElementSibling;o.appendChild(s),o.appendChild(e)}}function l(t){for(species of t){const e=document.querySelectorAll(`[data-species="${species}"] .Observation`).length.toString(),n=document.querySelector(`[data-species="${species}"] summary`);n.innerText=`${n.innerText} (${e})`}}const a=u(r);p(a,c),d(r),l(a)})();

Feedback

There you have it, my first bookmarklet. Let me know how it works for you.