MetaFilter Scroll Tag

By Plutor Last update Feb 25, 2012 — Installed 3,450 times.

There are 8 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name           MetaFilter Scroll Tag
// @namespace      http://plutor.org/
// @description    Tracks your last-read comment in threads, and allows you to jump back to it easily.
// @include        http://metafilter.com/*
// @include        http://*.metafilter.com/*
// ==/UserScript==
//
// OPERA USERS:
// This script has been tested as working in Opera 9.26 for Windows.  You will need
// to also add the Greasemonkey builtin function emulator script available here:
//     <http://userjs.org/scripts/browser/enhancements/aa-gm-functions>
//
// DONE 2011-12-07
// * Compressed cookie storage
//
// TODO
// * Use jQuery more widely
// * Mark the last comment if scrolled to the bottom (Opera)
// * Indicate marked comment(s) for "recent activity" page
// * Allow mark moving on "recent activity" page

// Content Scope 
var script = document.createElement('script');
script.appendChild(document.createTextNode('('+ everything.toString() +')();'));
script.setAttribute("type", "application/javascript");
(document.body || document.head || document.documentElement).appendChild(script);
function everything() {

// ============================================================================
// Intrepid MeFites who are dissatisfied with the markers and jumpers I have
// provided can use these objects to change them.  Making either one wider than
// 75 pixels is not advised, as that is how wide the left border is on
// MetaFilter. You may use this website to create the base64-encoded URLs:
//   <http://www.sveinbjorn.org/dataurlmaker>s
//
var markerconf = {
    width: '35',     // pixels
    height: '15',
    img: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAPCAYAAABut3YUAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9gDBA4sJYLQS4YAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAQ5JREFUSMfN1TFqhEAUxvF%2FYCEICaSzMGAKU60oOUGqbezSWbjX2ENs6xHiAewEm5ArRCyFKJmQUpFJ6aYKOJJkt3An%2B2CKYRjej2H4HsBO43oFbH6pM2BXVRU6SghBFEV10zT3QP0npiiKoyAMw8BxnL0gBbNareogCF7mxmRZdp3n%2Bd2%2BF1qML5mm%2BR7H8dPcGN%2F3H8Z7y7JIksSOouh5DFIwUkoJCB3%2F5yeQgun7XgJvaKopSMF0XacV8w3abrd2GIaPCqZt28%2ByLD90YoQQbDabGlgrmGEYzl3XvZq7oed5B%2BXOYnJ%2BCdz8VwBOMbfAeu7GUsqLQ5JYwaRpugSWx0jgQ0bCSc0mTmlqfwF0xNik%2FyQ5bQAAAABJRU5ErkJggg%3D%3D'
};
var jumperconf = {
    width: '15',
    height: '35',
    img: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAjCAYAAABLuFAHAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9gDBA8IJuvzka0AAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAYJJREFUSMel1LFugkAYwPE%2FaKpEu3UzOWOYTTp2c%2BniO%2FAGDr6Kmy%2Fg4GuUJ2jqTCMaxi4KKJhyXdBoyx1SSAgH3%2F2%2B7wMODEDyz60J4HleZWjbNiY1tuZ5sFqtmE6nn0KIL9Xk7Xb7NJvNBsPh8BYDjMfj9%2Fl8%2FqHCk8nkGRj8qQwgpZTAtwpnWZYVtg1wOp1SIFbhPF6M0zRNgUiFkyRJynCownm8GB%2BPx6SkshofDoczPq8643qcx5VY23Ycx6VY2XYeL8ZRFCUlldU4DENt5f1%2Bn9yD5dXDuhzzeDHe7XbaFZbHi3GWZaZhGE3NV2gqMdACuhrcKsOPGtzW4XYJrlW5Vaeytu0G8KDBDR3WLk%2Bg%2BD1bloVt26%2BdTudFJaMo6lqWdTk3AFnnp%2B8HQVAJ5vN9Exg5jnN3giAIcBzHB0ZGfq0vhHhbLBb9Xq9XCjebzQjwjauYNsFvWJS8L4RYu64rPc%2B77K7rSiHEGuiX3dZNgirwJsFyuawMLwkAVwd%2FAGRf4dA3CSb%2BAAAAAElFTkSuQmCC',
    maxopacity: 1.0,   // Out of 1.0
    fadeindur: 0.25    // Seconds
};

var cookie_limit = 4096; // Maximum cookie size

//
// ============================================================================

var thread_id;
var markedcomment;
var comments = new Array();
var lastscroll;
var nextshow;
var markerdat;

//
// mst_init
//
// Determines if this is a thread or not, and sets everything up
//
function mst_init() {
    thread_id = mst_threadUniqName(location.href);

    if (thread_id) {
        // This is a thread
        mst_initThread();

        // Watch for new comments
        mst_init_newcomments();
    } else if (mst_isRecentActivity(location.href)) {
        mst_initRecentActivity();
    } else {
        // This is not a thread
        mst_findThreads();
    }

}

//
// mst_initThread
//
// Initializes everything if this is a thread view
//
function mst_initThread() {
    mst_initComments();
    mst_loadValue();
    mst_showJumper(0);
    document.addEventListener('scroll', function() { mst_hideJumper() }, false);
    document.addEventListener('scroll', function() { mst_findTopComment() }, false);
    mst_findTopComment(); // Just to get us started

    // Focusing the comment textarea should set the tag to the bottom
    var newcomment = document.getElementById('comment');
    if (newcomment) {
        var y = mst_documentHeight();
        newcomment.addEventListener('focus', function() { mst_findTopComment(y) }, false);
    }
}

function mst_init_newcomments() {
    $("#newcomments").bind( 'mefi-comments', mst_initComments );
}

//
// mst_documentHeight
//
// Returns the total height of the entire document, in pixels
//
function mst_documentHeight() {
    if (window.scrollMaxY != null) {
        // Firefox
        return window.scrollMaxY + window.innerHeight;
    } else if (document.documentElement.clientHeight) {
        // Opera
        return document.documentElement.clientHeight;
    }
}

//
// mst_scrollLocation
//
// Returns the scroll position of the top of the screen, in pixels
//
function mst_scrollLocation() {
    if (window.scrollY != null) {
        // Firefox
        return window.scrollY;
    } else if (document.body.scrollTop) {
        // Opera
        return document.body.scrollTop;
    }
}

//
// mst_scrollBottom
//
// Returns the scroll position of the bottom of the screen, in pixels
//
function mst_scrollBottom() {
    return mst_scrollLocation() + window.innerHeight;
}

//
// mst_threadUniqName
//
// Argument: url = the url of the thread
//
// Returns a uniq name of the thread: "$subdomain$threadid"
//
function mst_threadUniqName(url) {
    var urlm = url.match(/^http:\/\/([^\/]*\.)?metafilter.com\/(\d+)\//);
    if (urlm)
        return urlm[1] + urlm[2];
    return "";
}

//
// mst_initComments
//
// Finds all of the comment DOM objects and puts them into an array
// for easy future use
//
function mst_initComments() {
    var alldivs = document.getElementsByTagName('div');
    for (var i=0; i<alldivs.length; ++i) {
        if (alldivs[i].className.match(/comments/) &&
            alldivs[i].id != 'prevDiv' && alldivs[i].id != 'prevDiv2' &&
            alldivs[i].innerHTML.match(/posted by/)) {
            comments.push(alldivs[i]);
        } // is a comment
    } // loop divs
}

//
// mst_findThreads
//
// Finds all of the threads listed on the page, and replaces the '(mm new)'
// listings with ones that are more accurate.
//
function mst_findThreads() {
    var alldivs = document.getElementsByTagName('span');
    for (var i=0; i<alldivs.length; ++i) {
        if (alldivs[i].className.match(/smallcopy/) &&
            alldivs[i].innerHTML.match(/posted by/)) {
            var metabits = alldivs[i].childNodes;
            // First pass - delete 'mm new' links
            for (var j=0; j<metabits.length; ++j) {
                var bit = metabits[j];
                var m;
                if (bit.tagName && bit.tagName.toLowerCase() == 'b' &&
                    bit.innerHTML.match(/\d+ new/)) {
                    bit.parentNode.removeChild(bit);
                }
            } // loop bits of info

            // Second pass - add new 'mm new' links
            for (var j=0; j<metabits.length; ++j) {
                var bit = metabits[j];
                var m;
                if ( bit.tagName && bit.tagName.toLowerCase() == 'a' &&
                     (m = bit.innerHTML.match(/^(\d+) (comment|answer)s?$/)) ) {
                    var total = m[1];
                    var tid = mst_threadUniqName(bit.href);
                    if (tid) {
                        var marked = mst_getValue(tid);
                        var newc = total - marked.num;
                        if (!marked) {
                            newc = total;
                            marked.id = '';
                        }
                        if (newc > 0) {
                            // There are new comments
                            var newnew = document.createElement('b');
                            newnew.style.color = 'white';
                            newnew.style.marginLeft = '0.4em';
                            newnew.innerHTML = '(<a href="'
                                             + bit.href
                                             + '#'
                                             + marked.id
                                             + '" target="_self" class="new">'
                                             + newc
                                             + ' new</a>)';
                            bit.parentNode.insertBefore( newnew,
                                                         bit.nextSibling );
                        }
                    }
                } // is a comments link
            } // loop bits of info
        } // is a thread
    } // loop divs

}

//
// mst_loadValue
//
// Argument: t = thread id (optional, defaults to global thread_id)
//
// Gets the comment id for the thread, finds the first comment after the
// anchor with that name, and marks it
//
function mst_loadValue(t) {
    if (!t) t = thread_id;
    var comment = mst_getValue(t);

    // Find the anchor
    var anchors = document.getElementsByTagName('a');
    for (var i=0; i<anchors.length; ++i) {
        if (comment && anchors[i].name == comment.id) {
            // Find the comment div after the anchor
            var a = anchors[i];
            while (a && (!a.className || !a.className.match(/comments/) &&
                         a.id != 'prevDiv' && a.id != 'prevDiv2')) {
                a = a.nextSibling;
            }
            
            if (a) {
                markedcomment = a;
                mst_mark(a);
            }

            break;
        }
    }
}

//
// mst_findTopComment
//
// Argument: ysc = number of pixels from the top (optional, defaults to current position)
// Argument: dropped = is this is a search caused by a dropped marker?
//
// If dropped is false, finds the first comment below the scroll point. If it's
// below the currently marked comment, it marks it.
// If dropped is true, finds the last comment above the scroll point and marks it.
//
function mst_findTopComment(ysc, dropped) {
    if (ysc == null) ysc = mst_scrollLocation();
    var topc;

    if (!dropped) {
        // Scroll to the last comment if we're scrolled to the bottom of the document
        if (ysc >= window.scrollMaxY)
            ysc = mst_documentHeight();

        // Don't do anything if this is the same place we already were
        if (ysc == lastscroll) return;
    }

    lastscroll = ysc;

    // Assuming the comments array is in order
    for (var i=0; i<comments.length; ++i) {
        var c = comments[i];
        var cy = c.offsetTop;

        if (!topc)
            topc = c;

        if (dropped) {
            // Trying to find the last comment above ysc
            if ( cy <= ysc ) {
                topc = c;
            } else {
                break;
            }
        } else {
            // Trying to find the first comment below ysc
            topc = c;
            if ( cy >= ysc ) {
                break;
            }
        }
    }

    if (topc && (dropped || !markedcomment || markedcomment.offsetTop < topc.offsetTop)) {
        mst_mark(topc);
    }
}

//
// mst_mark
//
// Argument: c = DOM object of the comment to mark
//
// Moves the marker to the comment indicated, and saves the comment id
//
function mst_mark(c) {
    var marker = document.getElementById("mst_marker");
    if (!marker) {
        // Create it
        marker = document.createElement('div');
        marker.id = "mst_marker";
        marker.style.backgroundImage = 'url(' + markerconf.img + ')';
        marker.style.backgroundRepeat = 'no-repeat';
        marker.style.width = "" + markerconf.width + 'px';
        marker.style.height = "" + markerconf.height + 'px';
        marker.style.position = 'absolute';
        marker.style.cursor = 'move';

        document.body.appendChild(marker);
        marker.addEventListener('mousedown', mst_grabMarker, false);
    }

    // Mark - top aligned
    marker.style.top = "" + (c.offsetTop) + "px";
    marker.style.left = "" + (75/2 - markerconf.width/2) + "px";

    markedcomment = c;
    var comment_id = mst_commentIdOf(c);
    var comment_num = mst_commentNumOf(c);

    // Save
    if (thread_id && comment_id && comment_num) {
        mst_setValue(thread_id, comment_id, comment_num);
    }
}

//
// mst_commentIdOf
//
// Argument: c = DOM object of the comment
//
// Returns the comment_id pulled from the name of the anchor immediately
// before the given comment
//
function mst_commentIdOf(c) {
    // Find the anchor right before the comment div
    while (c && (!c.tagName || c.tagName.toLowerCase() != 'a')) {
        c = c.previousSibling;
    }

    return (c) ? c.name : null;
}

//
// mst_commentIdOf
//
// Argument: c = DOM object of the comment
//
// Returns the index(+1) of the given comment in the comments array
//
function mst_commentNumOf(c) {
    for (var i=0; i<comments.length; ++i) {
        if (comments[i] == c) {
            return i+1;
        }
    }
    
    return 0;
}

//
// mst_showJumper
//
// Argument: fade = fraction from 0.0 to 1.0, defaults to 0
//
// Show the jumper with an opacity of $fade, calls itself with fade incremented
// by 0.1 until it's fully visible.
//
function mst_showJumper(fade) {
    if (!markedcomment) return;

    var winbottom = mst_scrollBottom();
    var commenty = markedcomment.offsetTop;

    if (commenty > winbottom) {
        // Show it
        var jumper = document.getElementById("mst_jumper");
        if (!jumper) {
            // Create it
            jumper = document.createElement('div');
            jumper.id = "mst_jumper";
            jumper.style.position = 'absolute';
            jumper.style.backgroundImage = 'url(' + jumperconf.img + ')';
            jumper.style.backgroundRepeat = 'no-repeat';
            jumper.style.height = "" + jumperconf.height + "px";
            jumper.style.width = "" + jumperconf.width + "px";
            jumper.style.cursor = 'pointer';

            var grabber = document.createElement('div');
            grabber.id = "mst_jumper_grabber";
            grabber.style.width = "" + jumperconf.width + "px";
            grabber.style.height = "" + (jumperconf.height / 3) + "px";
            grabber.style.position = 'relative';
            grabber.style.top = "0px";
            grabber.style.left = "0px";
            grabber.style.cursor = "move";

            jumper.appendChild(grabber);
            document.body.appendChild(jumper);

            jumper.addEventListener('mousedown', mst_grabMarker, false);
            jumper.addEventListener('click', mst_useJumper, false);
        }

        jumper.style.display = 'block';
        jumper.style.top = "" + (winbottom - jumperconf.height - 5) + "px";
        jumper.style.left = "" + (75/2 - jumperconf.width/2) + "px";

        if (!fade || fade < 0)
            fade = 0;

        // Amount to fade in by = 25msec / fadeindur;
        fade += (0.025 / jumperconf.fadeindur);

        if (fade < jumperconf.maxopacity) {
            jumper.style.MozOpacity = fade;
            jumper.style.opacity = fade;
            nextshow = setTimeout( mst_showJumper, 25, fade );
        } else {
            jumper.style.MozOpacity = jumperconf.maxopacity;
            jumper.style.opacity = jumperconf.maxopacity;
        }
    }
}

//
// mst_useJumper
//
// Jump to the marked comment. Called when the jumper is clicked.
//
function mst_useJumper() {
    // Go to the marked comment
    var id = mst_commentIdOf(markedcomment);
    if (id) location.href = "#" + id;
}

//
// mst_hideJumper
//
// Argument: keephidden = Don't schedule the showJumper call (defaults to 0)
//
// Hide the jumper immediately, and schedule a jumper re-show.  Keeps only a single
// re-show timer so it'll keep pushing it back until scrolling stops.
//
function mst_hideJumper(keephidden) {
    var jumper = document.getElementById("mst_jumper");

    if (jumper) {
        jumper.style.display = 'none';
    }

    if (nextshow) {
        clearTimeout(nextshow);
        nextshow = null;
    }

    if (!keephidden) nextshow = setTimeout( mst_showJumper, 350, 0 );
}

//
// mst_grabMarker
//
// Called when the marker or jumper are first grabbed for moving
//
function mst_grabMarker(e) {
    var grabber = document.getElementById('mst_marker');

    // Remove the mousedown event
    grabber.removeEventListener('mousedown', mst_grabMarker, false);

    // Add mousemove/mouseup events
    document.body.addEventListener('mousemove', mst_moveMarker, false);
    document.body.addEventListener('mouseup', mst_dropMarker, false);

    // Record where we started
    markerdat = {
                  x: e.pageX,
                  y: e.pageY,
                  g: grabber,
                  init: 0
                };

    e.preventDefault();
    return false;
}

//
// mst_moveMarker
//
// Called when a grabbed marker is moved. Moves the marker to where the mouse is,
// and tracks the location in the markerdat object.
//
function mst_moveMarker(e) {
    var x = e.pageX;
    var y = e.pageY;

    // Is this the initial movement
    if (!markerdat.init) {
        // Is initial movement sufficient?
        if ( Math.abs(markerdat.x-x) + Math.abs(markerdat.y-y) > 15 ) {
            // Hide the jumper, don't start redisplay timer
            markerdat.init = 1;
            mst_hideJumper(1);
        }
    }

    // Show the marker under the mouse
    if (markerdat.init) {
        var marker = document.getElementById('mst_marker');
        marker.style.MozOpacity = '0.7';
        marker.style.opacity = '0.7';
        marker.style.top = "" + (y - markerconf.height/2) + "px";
        marker.style.left = "" + (x - markerconf.width/2) + "px";
    }

    e.preventDefault();
    return false;
}

//
// mst_dropMarker
//
// Called when a grabbed marker is dropped. Finds the nearest comment, and marks it.
//
function mst_dropMarker(e) {
    // Did we ever have movement?
    if (markerdat.init) {
        var marker = document.getElementById('mst_marker');
        marker.style.MozOpacity = '1.0';
        marker.style.opacity = '1.0';

        // Record the new marked location
        mst_findTopComment(e.pageY, 1);
    }

    mst_showJumper(0);

    // Re-add the mousedown 
    markerdat.g.addEventListener('mousedown', mst_grabMarker, false);

    // Remove mousemove/mouseup events
    document.body.removeEventListener('mousemove', mst_moveMarker, false);
    document.body.removeEventListener('mouseup', mst_dropMarker, false);
}

//
// mst_isRecentActivity
//
function mst_isRecentActivity(url) {
    return (url.indexOf("http://www.metafilter.com/contribute/activity/") == 0);
}

function mst_initRecentActivity() {
    var alldivs = document.getElementsByTagName('div');
    for (var i=0; i<alldivs.length; ++i) {
        if (alldivs[i].style.fontWeight == 'bold' &&
            (m = alldivs[i].innerHTML.match(/(\d+) total comments.*most recent comment/))) {
            var total = m[1];
            var thisdiv = alldivs[i];
            var metabits = thisdiv.childNodes;

            for (var j=0; j<metabits.length; ++j) {
                var bit = metabits[j]

                if ( bit.tagName && bit.tagName.toLowerCase() == 'a' ) {
                    var tid = mst_threadUniqName(bit.href);
                    var url = bit.href.replace(/#\d+$/, '');
                    if (tid) {
                        var marked = mst_getValue(tid);
                        var newc = total - marked.num;

                        if (!marked) {
                            newc = total;
                            marked.id = '';
                        }
                        if (newc > 0) {
                            // There are new comments
                            var newdiv = document.createElement('div');
                            newdiv.style.fontWeight = 'bold';
                            newdiv.innerHTML = thisdiv.innerHTML.replace(/ total comments\..*/, "") +
                                " total comments.  " + newc + " since ";
                            
                            
                            var newcount = document.createElement('a');
                            newcount.target = bit.target;
                            newcount.href = url + '#' + marked.id;
                            newcount.innerHTML = 'the last comment you read';
                            newdiv.appendChild( newcount );

                            newdiv.innerHTML += ".  " +
                                thisdiv.innerHTML.replace(/.* total comments\./, "")
                            
                            thisdiv.parentNode.replaceChild(newdiv, thisdiv);
                        }
                        break;
                    }
                } // is a comment link
            }
        } // is a thread header
    }
}

//
// get/set helper functions
//

function mst_getValue(thread_id) {
    var subsite, thread_num;
    var parts = thread_id.match(/^([^\.]+)\.(\d+)$/);
    if (parts) {
        subsite = parts[1];
        thread_num = parts[2];
    } else {
        return
    }

    // Get the cookie for this subsite
    var cdata = Cookie.get("mst_" + subsite);
    var comment_id;
    var comment_num;
    if (cdata) {
        // Find the part for this thread, if it's there
        var thread_num = mst_toBase64(thread_num, 4);
        for (var i = 0; i < cdata.length; i += 10) {
            if (cdata.substr(i, 4) == thread_num) {
                comment_id = cdata.substr(i+4, 4);
                comment_num = cdata.substr(i+8, 2);
                break;
            }
        }

        if (comment_id && comment_num) {
            comment_id = mst_fromBase64(comment_id);
            comment_num = mst_fromBase64(comment_num);
        }
    }

    // Fallback and remove those cookies
    // We can probably remove this at some point
    if (!comment_id || !comment_num) {
        comment_id = Cookie.get("mst_" + thread_id);
        comment_num = Cookie.get("mst_" + thread_id + "num");

        // Remove the old cookie
        Cookie.unset("mst_" + thread_id, undefined, "metafilter.com");
        Cookie.unset("mst_" + thread_id + "num", undefined, "metafilter.com");

        if (comment_id && comment_num) { // Force a save
            mst_setValue(thread_id, comment_id, comment_num);
        }
    }

    return { "id": comment_id, "num": comment_num };
}
function mst_setValue(thread_id, comment_id, comment_num) {
    // Separate the subsite and thread_id
    var subsite, thread_num;
    var parts = thread_id.match(/^([^\.]+)\.(\d+)$/);
    if (parts) {
        subsite = parts[1];
        thread_num = parts[2];
    } else {
        return
    }

    // uuencode the three parts
    thread_num = mst_toBase64(thread_num, 4);
    comment_id = mst_toBase64(comment_id, 4);
    comment_num = mst_toBase64(comment_num, 2);

    // Get the cookie for this subsite
    var cdata = Cookie.get("mst_" + subsite);

    if (cdata) {
        // Remove the part for this thread, if it's there
        for (var i = 0; i < cdata.length; i += 10) {
            if (cdata.substr(i, 4) == thread_num) {
                cdata = cdata.substr(0, i) + cdata.substr(i+10);
                break;
            }
        }

        // Add this thread to the front
        cdata = thread_num + comment_id + comment_num + cdata;
    } else {
        cdata = thread_num + comment_id + comment_num;
    }

    // Trim
    // The cookie limit includes the cookie name
    if (cdata.length > cookie_limit - subsite.length - 5) {
        cdata = cdata.substr(0, Math.floor(cookie_limit/10)*10);
    }
    
    Cookie.set("mst_" + subsite, cdata, 10*365*24, undefined, "metafilter.com");
}
var base64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_/=";
function mst_toBase64(n, size) {
    var result = "";
    for (var i = 0; i < size; ++i) {
        var digit = n % 64;
        result = base64chars.charAt(digit) + result;
        n = (n - digit) / 64; 
    }
    return result;
}
function mst_fromBase64(n) {
    var result = 0;
    for (var i = 0; i < n.length; ++i) {
        var digit = n.charAt(n.length-i-1); // Get the nth from the last character
        var dval = base64chars.indexOf(digit) * Math.pow(64, i);
        result += dval;
    }
    return result;
}

/*****************************************************************************
 * Modified from cookie-js 0.4 by Maxime Haineault (max@centdessin.com)
 * <http://code.google.com/p/cookie-js/> 
 */
Cookie = {      
    /** Get a cookie's value */
    get: function(key) {
            // Still not sure that "[a-zA-Z0-9.()=|%/_]+($|;)" match *all* allowed characters in cookies
            tmp =  document.cookie.match((new RegExp(key +'=[a-zA-Z0-9.()=|%/_]+($|;)','g')));
            if(!tmp || !tmp[0]) return null;
            else return unescape(tmp[0].substring(key.length+1,tmp[0].length).replace(';','')) || null;
            
    },      
    
    /** Set a cookie */
    set: function(key, value, ttl, path, domain, secure) {
            cookie = [key+'='+    escape(value),
                              'path='+    ((!path   || path=='')  ? '/' : path),
                              'domain='+  ((!domain || domain=='')?  window.location.host : domain)];
            
            if (ttl)         cookie.push('Expires='+ Cookie.hoursToExpireDate(ttl));
            if (secure)      cookie.push('secure');
            return document.cookie = cookie.join('; ');
    },
    
    /** Unset a cookie */
    unset: function(key, path, domain) {
            path   = (!path   || typeof path   != 'string') ? '' : path;
    domain = (!domain || typeof domain != 'string') ? '' : domain;
            if (Cookie.get(key)) Cookie.set(key, '', 'Thu, 01-Jan-70 00:00:01 GMT', path, domain);
    },

    /** Return GTM date string of "now" + time to live */
    hoursToExpireDate: function(ttl) {
            if (parseInt(ttl) == 'NaN' ) return '';
            else {
                    now = new Date();
                    now.setTime(now.getTime() + (parseInt(ttl) * 60 * 60 * 1000));
                    return now.toGMTString();                       
            }
    }
}
/****************************************************************************/

//
// Start by initializing
//
mst_init();

// end of content scope
}