Bookmarklet: Scroll to the first new tweet

The Twitter web interface polls for new tweets and displays a notification when any are found. Clicking on that notification will show all of the new tweets. I've found there is a point with a relatively small number of new tweets – between 30 and 200 – where I want to skim them all, but it's a hassle to return to the position where I left off reading.

New Tweets Notification

The visual indicator for the newest tweet is very subtle and virtually unnoticeable while scrolling fast.

Newest Tweet Indicator - A slightly darker line

Solution

It's a fairly simple problem to solve with a bookmarklet – a tiny bit of javascript that is manually executed by opening a bookmark.

Right-click and bookmark the following link, or drag it to your bookmarks toolbar or menu: First New Tweet

This may not work in a feed reader. If that's the case, manually add the bookmarklet or view the original post. Internet Explorer may prompt you about adding a Favorite that is not safe; ignore that, or get a better browser.

There is a known bug (#86643) affecting some versions of Chrome that results in a bookmark that does not contain a name. Right click and Edit the bookmark to fix that.

To use the bookmarklet, open it the way you would a normal bookmark. If there are new tweets, the browser will automatically scroll to the oldest new tweet – which will be right above the last tweet visible prior to loading the newest tweets.

Read more about bookmarklets at Wikipedia.

Source Code

Unminified code, complete with a modified version of Dustin Diaz's getElementsByClass to accommodate Internet Explorer. I briefly tested this on Google Chrome 13, Firefox 4 and IE 8. This isn't meant to be pretty.

javascript:(function(){
  if( document.getElementsByClassName ) {
    var t=document.getElementsByClassName("last-new-tweet")[0];
    if(t) {
      window.scrollTo(0, t.offsetTop + t.offsetParent.offsetTop );
    }
  } else {
    var els = document.getElementsByTagName("div"), elsLen = els.length, pattern = new RegExp("(^|\\s)last-new-tweet(\\s|$)");
    for (var i = 0; i < elsLen; i++) {
      if ( pattern.test(els[i].className) ) {
       window.scrollTo(0, els[i].offsetTop + els[i].offsetParent.offsetTop );
       break;
      }
    }
  }
})();

Minified code, in case you'd like to manually create your own bookmarklet.

javascript:(function(){if(document.getElementsByClassName){var t=document.getElementsByClassName("last-new-tweet")[0];if(t){window.scrollTo(0,t.offsetTop+t.offsetParent.offsetTop);}}else{var els=document.getElementsByTagName("div"),elsLen=els.length,pattern=new RegExp("(^|\\s)last-new-tweet(\\s|$)");for(var i=0;i<elsLen;i++){if(pattern.test(els[i].className)){window.scrollTo(0,els[i].offsetTop+els[i].offsetParent.offsetTop);break;}}}})();

Print a MySQL-style table with PHP

I wanted to cleanly display tabular data from a PHP command line script, and I like how MySQL formats its output.  This is quick, somewhat inefficient, hacky code, but it got the job done. You'll find nothing to be proud of in this code; I don't want to spend more time writing this post about it than I did writing it :)

/**
 * Out a MySQL-style table of data
 *
 * @param	array  $data         Associative array of data to output. 
 * @param	array  $header_keys  Optional; Assoc array of display names to use for headers. Keys must match those of $data. Defaults to keys of $data.
 * @param	string $glue         String to join lines with, defaults to newline.
 * @return string
 *
 */
function build_table($data, $header_keys=array(), $glue="\n") {
  $table = '';
  $data_fmt = array();
  $divider_row = array();
  $header_fmt = array();
 
  $keys = array_keys($data[0]);
  $col_lengths = array_flip($keys); // used to determine the max column width
 
  if( empty($header_keys) ) {
    $header_keys = array_combine($keys, $keys);
  }
 
  // set the base max length to the length of our header keys
  foreach( $keys as $key ) {
    $col_lengths[$key] = strlen($header_keys[$key]);
  }
 
  foreach( $data as $row ) {
    foreach( $keys as $key ) {
      $col_lengths[$key] = max($col_lengths[$key], strlen($row[$key]));
    }
  }
 
  foreach( $keys as $key ) {
    $data_fmt[] = '%-' . $col_lengths[$key] . 's';
    $header_fmt[] = '%-' . $col_lengths[$key] . 's';
    $divider_row[] = str_pad('', $col_lengths[$key]+2, '-'); // fill the spacing
  }
  $data_fmt = '| ' . implode(' | ', $data_fmt) . ' |';
  $divider_row = '+' . implode('+', $divider_row) . '+';
  $header_fmt = '| ' . implode(' | ', $header_fmt) . ' |';
 
  // assemble the table
  $table .= $divider_row . $glue;
  $table .= vsprintf($header_fmt . $glue, $header_keys);
  $table .= $divider_row . $glue;
  foreach( $data as $row ) {
    $table .= vsprintf($data_fmt . $glue, $row);
  }
  $table .= $divider_row . $glue;
 
  return $table;
}

The output will be similar to:

+--------------------+--------------------+---------------+
| First              | Second col         | Random Column |
+--------------------+--------------------+---------------+
| 60386980488376     | 597655305189574649 | 51            |
| 959432             | 459191             | 396802        |
| 73874213           | 570702             | 771           |
| 144579584678722975 | 892300317939345    | 59180         |
| 293172             | 314                | 127725766727  |
+--------------------+--------------------+---------------+

It only works with associative arrays, but that's easy enough to fix. You can proudly test it out using this glorious snippet:

$data = array();
 
function get_rand() {
  $max=rand(1,6);
  $str='';
  for( $i = 0; $i < $max; $i++ ) {
    $str .= rand(10, 1000);
  }
  return $str;
}
 
for( $i = 0; $i < 5; $i++ ) {
  $row = array(
    'one'   => get_rand(),
    'two'   => get_rand(),
    'three' => get_rand(),
  );
  $data[] = $row;
}
 
$disp = array(
  'one'   => 'First',
  'two'   => 'Second col',
  'three' => 'Random Column',
);
 
echo build_table($data, $disp);

Generic Paging Links with ColdFusion

This is basically a port of the WordPress paginate_links function from PHP to ColdFusion, with a few enhancements. The original function was written by Michael D Adams (trac ticket).

Changes include the ability to control page size via a query string parameter, the ability to restrict the maximum page size (overriding the value of page_size) and a threshold (show_all_threshold) for displaying all pages. show_all_threshold is useful in cases where you may have a small number of pages and would like to display links to each of them.

Sample Usage

<cfparam name="url.pagenum" default="12" />
<cfparam name="url.nrecords" default="20" />
<cfoutput>
paginate_links(link_format="?pagenum=%PAGE%", end_size=2, mid_size=3, current=#url.pagenum#, total=260, page_size=#url.nrecords#)
#paginate_links(link_format="?pagenum=%PAGE%", end_size=2, mid_size=3, current=url.pagenum, total=260, page_size=url.nrecords)#
 
paginate_links(link_format="?pagenum=%PAGE%", end_size=2, mid_size=3, show_all_threshold=15, current=#url.pagenum#, total=350, page_size=#url.nrecords#)
#paginate_links(link_format="?pagenum=%PAGE%", end_size=2, mid_size=3, show_all_threshold=15, current=url.pagenum, total=350, page_size=url.nrecords)#
 
paginate_links(link_format="?pagenum=%PAGE%", end_size=2, mid_size=3, current=#url.pagenum#, total=300, page_size=#url.nrecords#
#paginate_links(link_format="?pagenum=%PAGE%", end_size=2, mid_size=3, current=url.pagenum, total=300, page_size=url.nrecords)#
</cfoutput>

Output

paginate_links(link_format="?pagenum=%PAGE%", end_size=2, mid_size=3, current=12, total=260, page_size=20)

« Previous12345678910111213Next »

paginate_links(link_format="?pagenum=%PAGE%", end_size=2, mid_size=3, show_all_threshold=15, current=12, total=350, page_size=20)

« Previous1291011121314151718Next »

paginate_links(link_format="?pagenum=%PAGE%", end_size=2, mid_size=3, current=12, total=300, page_size=20)

« Previous129101112131415Next »

Code

<cffunction name="paginate_links" access="public" output="false" returntype="any" hint="Return links to paginated results, heavily based on paginate_links() <http://trac.wordpress.org/ticket/3159> from WordPress <http://wordpress.org/> by Michael D Adams <http://blogwaffe.com/>">
  <cfargument name="link_format" required="YES" type="string" default="" hint="Link format.  Use %PAGE% for the page number placeholder (required), %SHOW% for the paging size (optional)" />
  <cfargument name="prev_next" required="YES" type="boolean" default="true" hint="Show previous/next links" />
  <cfargument name="prev_text" required="YES" type="string" default="&laquo;&nbsp;Previous" hint="Text for 'Previous Page' link" />
  <cfargument name="next_text" required="YES" type="string" default="Next&nbsp;&raquo;" hint="Text for 'Next Page' link" />
  <cfargument name="end_size" required="YES" type="numeric" default="1" hint="How many numbers on either end including the end" />
  <cfargument name="mid_size" required="YES" type="numeric" default="2" hint="How many numbers to either side of current not including current" />
  <cfargument name="current" required="YES" type="numeric" default="" hint="Currently active page" />
  <cfargument name="total" required="YES" type="numeric" default="1" hint="Total number of results.  Number of pages will be calculated by total/page_size" />
  <cfargument name="page_size" required="YES" type="numeric" default="20" hint="Number of results to show per page" />
  <cfargument name="max_page_size" required="YES" type="numeric" default="50" hint="Maximum number of results to show per page, to prevent abuse." />
  <cfargument name="show_all" required="YES" type="boolean" default="false" hint="Show all page numbers (ignore end_size, mid_size)" />
  <cfargument name="show_all_threshold" required="YES" type="numeric" default="0" hint="Show all page numbers ONLY if the total number of pages is <= this value.  0 to ignore." />
 
  <cfargument name="prev_class" required="YES" type="string" default="page-numbers pn-prev" hint="Class(es) to use for Previous Page link" />
  <cfargument name="next_class" required="YES" type="string" default="page-numbers pn-next" hint="Class(es) to use for Next Page link" />
  <cfargument name="current_class" required="YES" type="string" default="page-numbers pn-current" hint="Class(es) to use for Next Page link" />
  <cfargument name="dots_class" required="YES" type="string" default="page-numbers pn-dots" hint="Class(es) to use for dots span" />
  <cfargument name="page_numbers_class" required="YES" type="string" default="page-numbers" hint="Class(es) to use for page numbers" />
 
  <cfargument name="return_type" required="YES" type="string" default="text" hint="Valid types are array, text" />
 
  <cfscript>
	var page_links = ArrayNew(1);
	var link = '';
	var n = 0;
	var dots = false;
	var num_pages = Ceiling( arguments.total / arguments.page_size ); // Total number of pages
 
	// Sanity checks
  if( arguments.end_size LT 0 ) {
  	arguments.end_size = 1;
 	}
 
  if( arguments.mid_size LTE 0 ) {
  	arguments.mid_size = 2;
 	}
 
 	if( arguments.max_page_size ) {
 		arguments.page_size = Min( arguments.page_size, arguments.max_page_size );
 	}
 
 	// If we are within our show all threshold, enable the display of all pages
	if( arguments.show_all_threshold GTE num_pages ) {
		arguments.show_all = true;
	}
 
  // Add Previous Page link if current page is 2+, and we want to show next/prev links
	if( arguments.prev_next AND arguments.current AND arguments.current GT 1 ) {
		link = ReplaceNoCase( arguments.link_format, '%PAGE%', arguments.current - 1 );
		link = ReplaceNoCase( link, '%SHOW%', arguments.page_size );
		ArrayAppend(page_links, '<a class="#arguments.prev_class#" href="#link#">#arguments.prev_text#</a>' );
	}
 
	// Build internal page number links
	for( n = 1; n LTE num_pages; n=n+1 ) {
		if( n EQ arguments.current ) {
			ArrayAppend( page_links, '<span class="#arguments.current_class#">#n#</span>' );
			dots = true;
		} else {
			if( arguments.show_all OR ( n LTE arguments.end_size OR ( arguments.current AND n GTE arguments.current - arguments.mid_size AND n LTE arguments.current + arguments.mid_size ) OR n GT num_pages - arguments.end_size ) ) {
				link = ReplaceNoCase( arguments.link_format, '%PAGE%', n );
				link = ReplaceNoCase( link, '%SHOW%', arguments.page_size );
				ArrayAppend( page_links, '<a class="#arguments.page_numbers_class#" href="#link#">#n#</a>' );
				dots = true;
			} else if( dots AND NOT arguments.show_all ) {
				ArrayAppend( page_links, '<span class="#arguments.dots_class#">...</span>' );
				dots = false;
			}
		}
	}
 
  // Add Next Page link if current page LT total, and we want to show next/prev links
	if( arguments.prev_next AND arguments.current AND arguments.current LT num_pages ) {
		link = ReplaceNoCase( arguments.link_format, '%PAGE%', arguments.current + 1 );
		link = ReplaceNoCase( link, '%SHOW%', arguments.page_size );
		ArrayAppend(page_links, '<a class="#arguments.next_class#" href="#link#">#arguments.next_text#</a>' );
	}
 
 
  if( arguments.return_type EQ "array" ) {
  	return page_links;
  } else {
  	return ArrayToList(page_links, "");
  }
  </cfscript>
</cffunction>

Merge Structs in ColdFusion

Another helper function for ColdFusion, this one to replicate PHP's array_merge which allows you to merge multiple arrays. In this case I'm only concerned with associative arrays, which ColdFusion calls structs.

CFMX provides StructAppend which will merge two (and only two) structures, optionally overwriting keys. I always want to overwrite keys, and I want to merge unlimited structures.

<cfscript>
function struct_merge() {
	var base = {};
	var i = 1;
 
	for( i = 1; i LTE ArrayLen(arguments); i=i+1 ) {
		if( IsStruct(arguments[i]) ) {
			StructAppend(base, arguments[i], true);
		}
	}
	return base;
}
</cfscript>

Usage

<cfscript>
one = {
	a = "a",
	b = "b",
	c = "c",
	d = "d",
	e = "e"
};
 
two = {
	1 = "one",
	2 = "two",
	3 = "three",
	4 = "four",
	a = "one_SetByTwo",
	c = "three_SetByTwo"
};
 
three = {
	a = "one_SetByThree",
	2 = "b_SetByThree"
};
 
new = struct_merge(one, two, three);
</cfscript>
<cfdump var=#new#>

Result:


© 2007-2012, Corey Gilmore | Posts RSS Feed | Comments RSS Feed | Contact

 

The views expressed on these pages are mine alone and not those of any past or present employer. All information presented on this site was obtained lawfully and not through disclosure under the terms of an NDA.