Tech Note: The LGF Pages Bookmarklet Source Code

A deep dive into a Javascript pool
LGF • Views: 48,176
Image via Shutterstock

It’s been a while since I posted the source code for our LGF Pages bookmarklet, and it’s actually undergone some major changes recently, so let’s remedy that omission right now, shall we?

The biggest change: all of the code has been rewritten to use native Javascript, doing away with the need to load the jQuery library. This means the bookmarklet will open more quickly, since the new version only needs to load one external script instead of two.

I’ll start by posting the entire script, then I’ll take it apart section by section to explain how it all works.

/*
LGF Pages Posting Bookmarklet
By Charles F. Johnson
Copyright 2016 Little Green Footballs
All Rights Reserved.

File: lgf-postpage-loader.js
Last revision: 1/1/16 at 10:28 AM
*/
(function() {
	var url = location.href,
		d = document,
		ww = 720,
		wh = 250,
		version = '1.7.0',
		input,
		key,
		value,
		obj,
		theScript,
		noFunctions = RegExp([
			'(\.(?:pdf|jpe?g|gif|png|mp[34])(?:[\?#]|$))',
			'^https?://twitter\.com'
		].join('|'), 'i'),
		popup = window.open(
			'',
			'',
			'scrollbars=1,resizable=1,width=' + ww + ',height=' + wh
		);

	popup.document.write(
		'<!DOCTYPE html>' +
		'<html>' +
		'<head>' +
		'<meta charset="UTF-8">' +
		'<title>LGF Pages</title>' +
		'<meta name="viewport" content="width=device-width, initial-scale=1">' +
		'<style type="text/css">' +
		'*{' +
			'font-family:"Trebuchet MS",sans-serif;' +
			'font-weight:normal;' +
			'margin:0;' +
			'padding:0;' +
			'position:relative;' +
		'}' +
		'window{' +
			'width:' + ww + 'px;' +
			'height:' + wh + 'px;' +
		'}' +
		'body{' +
			'background:#D8E9D8;' +
			'background:linear-gradient(top, #D8E9D8, white) fixed;' +
		'}' +
		'img{' +
			'top:2px;' +
			'width:32px;' +
			'height:32px;' +
		'}' +
		'div{' +
			'width:560px;' +
			'background-color:white;' +
			'border:2px solid #B0B0B0;' +
			'border-radius:12px;' +
			'text-align:center;' +
			'margin:0 auto;' +
			'z-index:1;' +
			'top:-6px;' +
		'}' +
		'h1{' +
			'font-size:42px;' +
			'line-height:42px;' +
			'color:#548563;' +
			'letter-spacing:-0.09em;' +
			'text-align:center;' +
			'z-index:2;' +
		'}' +
		'h3{' +
			'font-size:220%;' +
			'margin:1em 0;' +
			'color:#548563;' +
		'}' +
		'</style>' +
		'</head>' +
		'<body>' +
		'<h1>lgf pages</h1>' +
		'<div>' +
		'<h3><img src="http://littlegreenfootballs.com/bigroller.gif"> Loading...</h3>' +
		'<form action="http://littlegreenfootballs.com/weblog/lgf-postpage.php" method="POST" accept-charset="UTF-8">' +
		'<input type="hidden" name="v" value="' + version + '">' +
		'</form>' +
		'</div>' +
		'</body>' +
		'</html>'
	);
	if (url.match(noFunctions)) {
		cantInject();
	}
	theScript = d.createElement('script');
	theScript.src = '//littlegreenfootballs.com/lgfjs/lgf-postpage-functions.min.js?' + Math.floor(new Date().getTime() / 1000);
	theScript.onload = function() {
		theScript.parentNode.removeChild(theScript);
		doForm($LGF_Page.getQuery(version));
	};
	theScript.onerror = function() {
		cantInject();
	};
	d.getElementsByTagName('head')[0].appendChild(theScript);
	function doForm(obj) {
		var popForm = popup.document.forms[0];
		for (key in obj) {
			input = popup.document.createElement('input');
			input.type = 'hidden';
			input.name = key;
			input.value = obj[key];
			popForm.appendChild(input);
		}
		popForm.submit();
	}
	function cantInject() {
		obj = {
			u: url,
			x: 1
		};
		if (typeof d.title != 'undefined') {
			obj.t = d.title;
			obj.s = d.selection ? d.selection.createRange().text : d.getSelection();
		}
		doForm(obj);
	}
})();

OK. Now that I’ve inflicted all that Javascript upon you, dear reader, let’s dig in and explain what’s going on.

Starting at the top, this entire bookmarklet is wrapped in something known as a self-executing anonymous function (SEAF), so that all the functions and variables contained within it remain private and are only available in the scope of the self-executing function. This is a very important concept for bookmarklets; it ensures that variables can’t conflict with any global variables on the web page in which you invoke the bookmarklet. Because that would be a BAD THING.

It’s “self-executing” because it immediately executes as soon as it’s defined, and “anonymous” because it has no function name. A SEAF typically looks like this:

(function(){
 // the code
})();

And if you look at the beginning and ending lines of our script, you’ll see that all of the code is contained within a SEAF.

The first order of business is defining variables that will be needed within the script; I like to do this at the beginning of a block of code so all the variable definitions are in one place. That’s what lines 11 through 29 do.

But there are a couple of tricky things going on in there. For example:

noFunctions = RegExp([
	'(\.(?:pdf|jpe?g|gif|png|mp[34])(?:[\?#]|$))',
	'^https?://twitter\.com'
].join('|'), 'i')

The “noFunctions” variable is actually a regular expression that checks the URL of the page in which the bookmarklet is invoked, to see if it needs special attention. PDFs, images, and videos can open in a browser window, but they aren’t actual web pages with a Document Object Model, so we need to handle them differently.

This regular expression also checks to see if the current page is on twitter​.com, because Twitter uses special HTTP headers that prevent bookmarklets from injecting script tags — a feature of the LGF Pages bookmarklet we’re about to describe.

I wrote this as an array of strings that are joined together with the regular expression OR operator, so that I can easily add any websites to the list that need the same kind of special treatment as twitter​.com.

The “popup” variable is important to the rest of the code; this variable declaration opens the LGF Pages popup window and returns a reference to the new window.

popup = window.open(
	'',
	'',
	'scrollbars=1,resizable=1,width=' + ww + ',height=' + wh
)

That brings us to lines 31 through 94. This section uses Javascript’s document.write() function to create a web page inside our popup window, that serves as a placeholder while the rest of the script executes, and also contains an HTML form that will be used to send the rest of the page data to the LGF Pages posting script. I broke this section up into a series of strings to make the structure more obvious (and easier to modify as needed).

Lines 95 through 97 use that regular expression we defined above to check the current web page’s URL. If it matches one of the extensions or full URLs in the regex, we jump to the cantInject() function (which I’ll describe soon).

if (url.match(noFunctions)) {
	cantInject();
}

If it’s OK to inject a script tag into the current page’s DOM, we then proceed to do exactly that. Lines 98 through 107 create a script element and set its src attribute to point to the external script containing all the special functions we use to examine a web page and discover any embedded video, audio and PDF files that will be automatically included in the LGF Page posting window.

theScript = d.createElement('script');
theScript.src = '//littlegreenfootballs.com/lgfjs/lgf-postpage-functions.min.js?' + (new Date().getTime());
theScript.onload = function() {
	theScript.parentNode.removeChild(theScript);
	doForm($LGF_Page.getQuery(version));
};
theScript.onerror = function() {
	cantInject();
};
d.getElementsByTagName('head')[0].appendChild(theScript);

We want to prevent the user’s browser from caching the script so that it’s loaded every time the bookmarklet opens, so we append the current time in milliseconds to the script’s address as a query string (following a question mark). This lets us modify the external functions as needed, and know they’ll always be reloaded the next time the bookmarklet is invoked.

We use the script element’s onload callback to invoke the external script’s code when it’s finished loading and initializing; the $LGF_Page.getQuery() function returns an object which we pass to the doForm() function.

If there’s some kind of error that causes the load to fail, the onerror callback jumps to the cantInject() function.

Now we get to the real meat of the enchilada, the function that actually sets up the form in our popup window, fills in the various fields with data, and submits it to the LGF Pages script: doForm(). This function is passed an object, and uses a for/in loop to create hidden inputs that use the object keys as the names of the input elements and the object values as the input elements’ values, appends each new input element to the form, then submits the form. This causes the popup window to come up with the LGF Pages posting form you know and love, with all the fields filled in.

function doForm(obj) {
	var popForm = popup.document.forms[0];
	for (key in obj) {
		input = popup.document.createElement('input');
		input.type = 'hidden';
		input.name = key;
		input.value = obj[key];
		popForm.appendChild(input);
	}
	popForm.submit();
}

Previous versions of the LGF Pages bookmarklet sent all this data to the posting script by using a URL and query string (known as the HTML GET method), but this has severe limitations on length. This new version uses the POST method by submitting a form, which gets rid of the length limits.

Lines 119 through 129 contain the cantInject() function mentioned above. This is the function that’s invoked when we determine that the page we’re on prevents us from injecting script tags, or if there’s an error when we try to load the external function script. This function creates an object to pass to doForm(), containing the URL of the page we’re on and the title and any selected text (if they exist).

function cantInject() {
	obj = {
		u: url,
		x: 1
	};
	if (typeof d.title != 'undefined') {
		obj.t = d.title;
		obj.s = d.selection ? d.selection.createRange().text : d.getSelection();
	}
	doForm(obj);
}

OK. We made it all the way to the end of the lgf-postpage-loader.js bookmarklet. Give yourself a hand if you followed along this far.

But there’s another part to this software entity, the external script called lgf-postpage-functions.js. Just so we have a sense of closure, I’ll post that script now; but I’m not going to do a complete walk-through because I can’t even. I’ll just tell you that this script basically consists of one function defined as part of a global object named $LGF_Page, whose purpose is to find embedded video, audio and other files that would be nice to include in an LGF Page, and also find the underlying HTML code for any selected text. I’ll leave it as an exercise for the coding-inclined to figure out exactly how it does that.

This is loaded as an external file so that it can be updated and modified as needed, when new types of files are identified and new websites added to the list, without making you, dear user, reinstall the bookmarklet for every change.

So without further ado — the rest of the story.

/*
LGF Pages Bookmarklet Functions
Copyright 2016 Little Green Footballs
All Rights Reserved.

File: lgf-postpage-functions.js
Last revision: 1/1/16 at 5:13 PM
*/
var $LGF_Page = {
	version: '1.4.0',
	getQuery: function(loader) {
		if (typeof loader == 'undefined' || parseFloat(loader) < 1.5) {
			alert('This version of the LGF Pages bookmarklet is out of date! Please reinstall the bookmarklet to get the latest version.');
			return;
		}
		var getSelectionHtml = function() {
				var html = '';
				if (typeof window.getSelection != 'undefined') {
					var sel = window.getSelection();
					if (sel.rangeCount) {
						var container = document.createElement('div');
						for (var i = 0, len = sel.rangeCount; i < len; ++i) {
							container.appendChild(sel.getRangeAt(i).cloneContents());
						}
						html = container.innerHTML;
					}
				} else if (typeof document.selection != 'undefined') {
					if (document.selection.type == 'Text') {
						html = document.selection.createRange().htmlText;
					}
				}
				return html;
			},
			ob,
			video = image = audio = iframe = '',
			url = location.href,
			d = document,
			description = getSelectionHtml();
		if (url.match(/www\.npr\.org/i)) {
			ob = d.getElementsByClassName('download');
			if (ob.length) {
				audio = ob[0].getAttribute('href').split('?')[0];
			}
		} else if (url.match(/^http:\/\/www\.msnbc\.com\/[^\/]*\/watch\//i)) {
			ob = d.querySelectorAll('meta[itemprop="embedURL"]');
			if (ob.length) {
				iframe = ob[0].getAttribute('content');
			}
		} else if (url.match(/^http:\/\/www\.msnbc\.com\//i)) {
			ob = d.querySelectorAll('meta[property="nv:videoId"]');
			if (ob.length) {
				iframe = 'http://player.theplatform.com/p/7wvmTC/MSNBCEmbeddedOffSite?guid=' + ob[0].getAttribute('content');
			}
		} else if (url.match(/^http:\/\/www\.nbcnews\.com\//i)) {
			ob = d.getElementById('embed');
			if (ob) {
				var thediv = d.createElement('div');
				thediv.innerHTML = ob.value;
				iframe = thediv.firstChild.getAttribute('src');
			} else {
				ob = d.querySelectorAll('meta[itemprop="embedUrl"]');
				if (ob.length) {
					iframe = ob[0].getAttribute('content');
				}
			}
		} else if (url.match(/video\.pbs\.org\/video\//i)) {
			iframe = 'http://video.pbs.org/viralplayer/' + url.split('/')[4];
			ob = d.querySelectorAll('meta[property="og:image"]');
			if (ob.length) {
				image = ob[0].getAttribute('content');
			}
		} else if (url.match(/www\.rightwingwatch\.org/i)) {
			ob = d.getElementById('content-area').getElementsByTagName('embed');
			if (ob.length) {
				if (ob[0].getAttribute('src').match(/www\.youtube\.com\/v\//i)) {
					iframe = 'https://www.youtube.com/embed/' + ob[0].getAttribute('src').split('?')[0].split('/')[4];
				}
			} else {
				ob = d.getElementById('content-area').getElementsByTagName('iframe');
				if (ob.length) {
					iframe = ob[0].getAttribute('src');
					if (iframe.substring(0, 2) == '//') {
						iframe = 'http:' + iframe;
					}
				}
			}
		}
		if (iframe == '') {
			var embeds = RegExp([
				'//www\.youtube\.com/embed',
				'player\.vimeo\.',
				'w\.soundcloud\.com/player',
				'www\.scribd\.com/embeds/',
				'media\.mtvnservices\.com/embed/',
				'mediamatters\.org/embed/static/clips/'
			].join('|'), 'i');
			ob = [].slice.call(d.querySelectorAll('iframe'));
			if (ob.length) {
				ob = ob.filter(function(e) {
					return ((e.src.match(embeds) || (e.hasAttribute('data-src') && e.getAttribute('data-src').match(/\/\/www\.youtube\.com\/embed/i))) && !e.classList.contains('gaTrackYouTube'));
				});
				if (ob.length && !url.match(/www\.youtube\.com\/|vimeo\.com\/|soundcloud\.com|www\.scribd\.com/i)) {
					ob = ob[0];
					if (ob.hasAttribute('data-src')) {
						iframe = ob.getAttribute('data-src');
					} else {
						iframe = ob.getAttribute('src');
					}
					if (iframe.substring(0, 2) == '//') {
						iframe = 'http:' + iframe;
					}
				}
			}
		}
		return {
			u: url,
			t: d.title,
			f: iframe,
			m: video,
			a: audio,
			i: image,
			s: description
		};
	}
};

Jump to bottom

29 comments
1
thedopefishlives  Jan 3, 2016 • 6:15:53pm

Random question. You said in the note that you switched from using HTTP GET to HTTP POST - is there a reason why you chose GET in the first place, given that form submission is basically why the POST verb was created in the first place?

2
Ace-o-aces  Jan 3, 2016 • 6:16:17pm

Today in false equivalence: From a Bundy defender…

3
Backwoods_Sleuth  Jan 3, 2016 • 6:17:49pm

Do we have to replace our bookmarklet bookmark?

4
Charles Johnson  Jan 3, 2016 • 6:20:03pm

re: #1 thedopefishlives

Random question. You said in the note that you switched from using HTTP GET to HTTP POST - is there a reason why you chose GET in the first place, given that form submission is basically why the POST verb was created in the first place?

Because it was easier.

5
thedopefishlives  Jan 3, 2016 • 6:20:57pm

re: #4 Charles Johnson

Because it was easier.

Spoken like a true programmer.

6
Targetpractice  Jan 3, 2016 • 6:21:05pm

Thank you, Captain Obvious.

7
Belafon  Jan 3, 2016 • 6:22:39pm

re: #6 Targetpractice

[Embedded content]

Thank you, Captain Obvious.

Yes, but sometimes the guy with a uniform has to say it.

8
Ace-o-aces  Jan 3, 2016 • 6:25:56pm

OK, you can stop internetting for today, this guy has won it:

9
The Vicious Babushka  Jan 3, 2016 • 6:26:47pm

STUPIDEST MEME OF THE DAY==>
This illiterate probably calls college-educated people, “Libtards”

10
Camacho DeezNuts 2016  Jan 3, 2016 • 6:27:47pm

re: #8 Ace-o-aces

Hm… that’s good. How about this:

11
Targetpractice  Jan 3, 2016 • 6:28:51pm

re: #9 The Vicious Babushka

STUPIDEST MEME OF THE DAY==>
This illiterate probably calls college-educated people, “Libtards”

[Embedded content]

I doubt any of those camped out there own a single parcel of land in Oregon. The guys who do have chosen to quietly surrender themselves to authorities and serve out the remainder of their sentences for arson which put the lives of firefighters in jeopardy.

12
Great White Snark  Jan 3, 2016 • 6:29:35pm

This is the first version that updates itself right? Need to remove the old ones from my various computers?

13
Bill and Opus for 2016!  Jan 3, 2016 • 6:29:54pm

I notice that .webm (compact imbedded video) files aren’t included in your noFunctions definitions. Do they just not require special handling in the fashion that other graphic/sound media types do?

14
Backwoods_Sleuth  Jan 3, 2016 • 6:30:36pm
15
thedopefishlives  Jan 3, 2016 • 6:30:39pm

re: #13 Bill and Opus for 2016!

I notice that .webm (compact imbedded video) files aren’t included in your noFunctions definitions. Do they just not require special handling in the fashion that other graphic/sound media types do?

Probably because .webm files can’t be displayed as standalone documents.

16
Cheechako  Jan 3, 2016 • 6:34:44pm

Once the Nevada BLMers get back from their holidays (use or loose vacation time) they might want to head out to the Bundy Ranch. Looks like nobody is home. Time to round up the trespassing cattle.

17
Belafon  Jan 3, 2016 • 6:37:42pm

re: #16 Cheechako

Dad’s still at home.

18
wheat-dogghazi-mailgate  Jan 3, 2016 • 6:38:26pm

Charles, since we’re discussing tech issues, how do you update the LGF Facebook page? Do you do it manually, or is there some spiffy FB API that updates whenever there’s a new post here?

I use a WordPress plugin for my site, which automatically posts to my FB page, but periodically I have to reauthorize it, because FB “forgets” it.

19
William Lewis  Jan 3, 2016 • 6:38:43pm

re: #17 Belafon

Dad’s still at home.

Good opportunity to put him in a cell for a nice long time.

20
The Vicious Babushka  Jan 3, 2016 • 6:40:48pm

U MAD BRO

21
Targetpractice  Jan 3, 2016 • 6:41:17pm

re: #19 William Lewis

Good opportunity to put him in a cell for a nice long time.

Yeah, what are the boys and their schoolyard buddies going to do, hop the next flight back home while BLM is executing the court order? Might be a good idea, before doing so, to announce that anybody who leaves the refuge will immediately be arrested.

22
Targetpractice  Jan 3, 2016 • 6:42:41pm

re: #20 The Vicious Babushka

U MAD BRO

[Embedded content]

Got the tinfoil beanie on a little tight, don’t he?

23
Camacho DeezNuts 2016  Jan 3, 2016 • 6:49:48pm

re: #20 The Vicious Babushka

Let go of the hate in your heart you no good piece of shit. LOL

24
goddamnedfrank  Jan 3, 2016 • 6:58:09pm
25
The Vicious Babushka  Jan 3, 2016 • 6:58:45pm

re: #24 goddamnedfrank

[Embedded content]

He seems nice.

26
Camacho DeezNuts 2016  Jan 3, 2016 • 7:01:24pm

re: #25 The Vicious Babushka

He seems nice.

No hate in his heart at all. None. Not a smidge of hate. Nothing.

27
Charles Johnson  Jan 3, 2016 • 7:27:20pm

re: #18 wheat-dogghazi-mailgate

Charles, since we’re discussing tech issues, how do you update the LGF Facebook page? Do you do it manually, or is there some spiffy FB API that updates whenever there’s a new post here?

I use a WordPress plugin for my site, which automatically posts to my FB page, but periodically I have to reauthorize it, because FB “forgets” it.

The posting script automatically updates the LGF Facebook page. The authorization token from Facebook expires every 60 days or so, so my script actually renews the authorization token when that happens.

This was a major pain to get working, but now it’s all automatic.

28
wheat-dogghazi-mailgate  Jan 3, 2016 • 7:44:42pm

re: #27 Charles Johnson

The posting script automatically updates the LGF Facebook page. The authorization token from Facebook expires every 60 days or so, so my script actually renews the authorization token when that happens.

This was a major pain to get working, but now it’s all automatic.

I am in awe.

29
Eventual Carrion  Jan 3, 2016 • 8:47:45pm

re: #6 Targetpractice

[Embedded content]

Thank you, Captain Obvious.

They learned well from the Benghazi terrorists. Blend in with an ongoing protest and launch your armed terrorism under their cover.


This article has been archived.
Comments are closed.

Jump to top

Create a PageThis is the LGF Pages posting bookmarklet. To use it, drag this button to your browser's bookmark bar, and title it 'LGF Pages' (or whatever you like). Then browse to a site you want to post, select some text on the page to use for a quote, click the bookmarklet, and the Pages posting window will appear with the title, text, and any embedded video or audio files already filled in, ready to go.
Or... you can just click this button to open the Pages posting window right away.
Last updated: 2023-04-04 11:11 am PDT
LGF User's Guide RSS Feeds

Help support Little Green Footballs!

Subscribe now for ad-free access!Register and sign in to a free LGF account before subscribing, and your ad-free access will be automatically enabled.

Donate with
PayPal
Cash.app
Recent PagesClick to refresh
Texas County at Center of Border Fight Is Overwhelmed by Migrant Deaths EAGLE PASS, Tex. - The undertaker lighted a cigarette and held it between his latex-gloved fingers as he stood over the bloated body bag lying in the bed of his battered pickup truck. The woman had been fished out ...
Cheechako
3 days ago
Views: 152 • Comments: 0 • Rating: 1