Bookmarks have their place, but sometimes what you'd really like to be able to do is save actual snippets of content from a given web page rather than just keep a reference to the page's URL in your Bookmarks menu. Saving the whole page is easy, but usually impractical, because usually you're not interested in saving all the text (and photos and advertisements, etc.) on a page. Usually you're just interested in a particular span of text (a code snippet, or a particular paragraph, or a list of links) and nothing more. There ought to be an easy way to cache just the portion of the page that interests you. And there is -- if you have Day CRX and Google's Chrome browser.
It turns out that with only around 200 lines of JavaScript, you can modify Chrome's behavior so that when you make a text selection (or in fact any selection) on a web page, a button appears on the page, and when you click the button, your selection is saved to your Sling repository. (In this case, we're using CRX instead of Sling per se, but the same code should work with either one.)
The following code (stored in a file called clip2crx.user.js) shows how it's done. Note that the code is formatted as a Greasemonkey script (but has no Greasemonkey dependencies). It turns out that Chrome version 4-and-up supports Greasemonkey scripts: All you have to do to load such a script (assuming its name ends in ".user.js") is Open the script in Chrome and say yes to the "Are you sure you want to install this script?" warning that appears. No need to restart Chrome. Once the script is loaded, simply go to any web page and make a selection on the page (by click-dragging the mouse). When you let your finger off the mouse, a "Save Selection" button will appear in the lower right corner of the window. Click that button. A small dialog will pop up, asking you to give the selection a name. After supplying a name, click OK and the selection is saved to CRX.
// ==UserScript==
// @name clip2crx
// @namespace clip2crx
// @description Save user-selected items in web page to a CRX node
// @include *
// ==/UserScript==
// Kas Thomas <kas.thomas@day.com>
// 28 June 2010
// Tested in Google Chrome 5.0.375.70 beta
// *************************** Button ******************************
// This is the "Save Selection" button (sans event handlers).
Button = new function( ) { // Button is a singleton object
var _ID = "SAVESELECTION";
var _domNode =
createFixedPositionNode( "div", _ID, 50, 10,
'<input type="button" value="Save Selection..." />' );
this.getDomNode = function ( ) { return _domNode; }
function createFixedPositionNode( nodeName, id, x, y, innerhtml ) {
var node = document.createElement( nodeName );
var style = 'position: fixed; ';
style += 'bottom: ' + y + 'px; ';
style += 'right: ' + x + 'px; ';
style += 'z-index:100; ';
style += 'font-size: small;';
node.setAttribute( "style", style );
node.setAttribute( "id", id );
node.innerHTML = innerhtml;
return node;
}
// This will add the Button to the page
this.attach = function ( doc ) {
if ( !nodeExists( doc, _ID ) )
doc.body.insertBefore( _domNode , doc.body.firstChild );
}
// This removes the Button from the page
this.detach = function ( doc ) {
if ( nodeExists( doc, _ID ) )
_domNode = doc.body.removeChild( _domNode );
}
function nodeExists( doc, id ) {
return doc.getElementById( id ) != null;
}
}; // Button
// called by buttonHandler( ) further below...
function saveToRepository() {
// Repository storage URL
var base_url = "http://localhost:7402/content/myapp/";
// Obtain the content we want to post
var params = "content=" + getContent( );
// prompt the user to give the new node a name
var name = prompt( "Name for this entry:" );
if ( !name || name.length == 0 )
throw "No name provided.";
params += "&:nameHint=" + name;
// Prepare for AJAX POST
http = new XMLHttpRequest( );
http.open( "POST", base_url, true );
// Send the proper header information along with the request
http.setRequestHeader( "Content-type", "application/x-www-form-urlencoded" );
http.setRequestHeader( "Content-length", params.length );
http.setRequestHeader( "Connection", "close" );
// Show whether we succeeded...
http.onreadystatechange = function( ) {
if ( http.readyState == 4 )
;// alert( "http.status = " + http.status ); DEBUG ONLY
}
// do the AJAX POST
http.send( params );
}
function getContent() {
var selection = window.getSelection( );
var markup = serializeSelection( selection );
var finalMarkup = formatPage( markup );
return finalMarkup;
}
function formatPage( pageContent ) {
return "<br>" + fixRelativeURLs( pageContent );
}
// Try to expand relative URLs to absolute paths
// so they'll continue to work when viewed from CRX
function fixRelativeURLs( text ) {
var url = window.parent.location.href;
var lastSlash = url.lastIndexOf("/");
var basisURL = url.substring( 0, lastSlash );
function convertURL( hit, offset, text ) {
var output = hit;
if ( text[ offset + hit.length ] == "/" )
output += window.parent.location.protocol +
"//" + window.parent.location.host;
else if ( text[ offset + hit.length ].match( /\w|#|\./) != null )
output += basisURL + "/";
return output; // all other cases, nop
}
var regex1 = /src="(?!http)/gi;
var regex2 = /href="(?!http)/gi;
var newText =
text.replace( regex1, convertURL ).replace( regex2, convertURL );
return newText;
}
function serializeSelection( selection ) {
var xmlFragment = "";
try {
var n = 0, ranges = selection.rangeCount;
while ( n != ranges ) {
var range = selection.getRangeAt( n++ );
var content = range.cloneContents( );
var serializer = new XMLSerializer( );
xmlFragment += serializer.serializeToString( content );
}
}
catch( msg ) { }
return xmlFragment;
}
// ******************************** main( ) ********************************
( function main( ) {
// Button mouseup handler
function buttonHandler( e ) {
Button.detach( document );
saveToRepository();
}
// document.body mouseUpHandler
function mouseUpHandler( e ) {
var selection = window.getSelection( );
if ( selection.toString( ).length > 0 ) { // a selection exists
Button.attach( document );
}
else
Button.detach( document );
}
function clickHandler( e ) { // need to handle page clicks too, because
// if user clicks inside a selection,
// browser eats the mouseup event...
mouseUpHandler( e );
}
// now add event handlers to the page...
addEventListener( "mouseup", mouseUpHandler, false );
addEventListener( "click", clickHandler, false );
// add event handler to the Button...
Button.getDomNode( ).addEventListener( "mouseup", buttonHandler, false );
}) ( ); // end main( )
There are several things to note.
By default, selections are stored in CRX at a URL of:
http://localhost:7402/content/myapp/[name]
where name is the item name you provided in the popup dialog. The name is provided to CRX as a :nameHint parameter value in the POST data. This ensures that the item gets stored with the name you want (rather than an autogenerated number created by Sling) while also ensuring that the name is normalized to a legal form (with non-alphanumeric characters converted to underscores). The fact that it is stored under /content/ means that Sling will look for application scripts to run against your content, under http://localhost:7402/apps. In this example we're not using any such scripts, but it would be easy to add some.
Content gets stored in CRX with all markup intact, so that when you decide to view the snippet later, it will have more or less than same appearance that it had on the original web page from which it was taken. Also note that, thanks to a function called fixRelativeURLs(), any relative URLs in the saved selection are expanded to full-path form before content is moved into the repository. That way, embedded links should still work properly when you view the stored snippet later even though its root URL has changed.
The rest of the code is fairly self-explanatory.
To obtain your own ready-to-run copy of clip2crx.user.js, check out the package stored here.