safari-articles.js Safari Code | Browsers

Standards Based Development

safari-articles.js


// feed://__rsrc__/__rsrc__/Articles.js
// in safari, found on feed://images.apple.com/main/rss/hotnews/hotnews.rss
//var sUniqueCounter = 0;
var sSendCommands = true;      // if false, makes sendSyndicationCmd a no-op unless reloadArticles is set

var sReadClass            = "apple-rss-article apple-rss-read";
var sUnreadClass          = "apple-rss-article apple-rss-unread";
var sSelectedClass        = "apple-rss-selected";
var sBookmarkedClass      = "apple-rss-bookmarked";
var sCurrentClass         = "apple-rss-current";

var sCountsId             = "apple-rss-counts";
var sTotalCountId         = "apple-rss-total-count"; // The visible total count
var sTotalArticleCountId  = "apple-rss-total-article-count"; // A span that can arrive on reload with new counts
var sUnreadCountWrapperId = "apple-rss-unread-count-wrapper";
var sUnreadCountId        = "apple-rss-unread-count";
var sAlertBannerId        = "apple-rss-alert-banner";
var sAlertUrlId           = "apple-rss-alert-url";
var sAlertTextId          = "apple-rss-alert-text";
var sAlertPasswordFormId  = "apple-rss-alert-form";

var sBookmarkLinkId       = "apple-rss-bookmark-link";
var sBookmarkHrefId       = "apple-rss-bookmark-href";
var sSearchfieldId        = "apple-rss-search-field";
var sTunesLinkId          = "apple-rss-itunes-link";
var sTimespansId          = "apple-rss-timespans";
var sSourcelistId         = "apple-rss-source-list";

var sInfoId               = "AppleSyndicationInfo"; // Do not change this without ensuring a commensurate change is made in the browser
var sContentId            = "apple-rss-content";
var sSliderId             = "apple-rss-slider";
var sScrollerId           = "apple-rss-scroller";
var sSidebarId            = "apple-rss-sidebar";
var sSortsId              = "apple-rss-sorts";
var sNoArticlesId         = "apple-rss-no-articles";

var sCurrentArticlesId    = "apple-rss-current-articles";
var sPaginationId         = "apple-rss-pagination";
var sNextArticlesId       = "apple-rss-next-articles";
var sNextArticleId        = "apple-rss-next-article";
var sPreviousArticlesId   = "apple-rss-previous-articles";
var sPreviousArticleId    = "apple-rss-previous-article";

var sTitleTextId          = "apple-rss-title-text";
var sSourceNameTextId     = "apple-rss-source-name-text";
var sFOAFPlaceholderId    = "apple-rss-foaf-placeholder";

var sIconLinkId           = "apple-rss-icon";


// Track all of the unread articles we have seen,
// so we can render them as unread in subsequent database round trips
var sUnreadIds = new Array();

// The following are controlled via user defaults and set from Feed.html template
var sMinFirstDate;
var sMaxLastDate;
var sClickToMarkRead;


function requestForSyndicationCmd( cmd, arg, async )
{
	if( async == undefined ) reloadArticles = true;
    var url = document.location.protocol+"//"+document.location.hostname;
    var port = document.location.port;
    if ( port!=null )
        url += ":"+port;
    url += "/__cmd__/"+cmd;
    if( arg != null )
        url += "/"+arg; 
    //url += "/"+(++sUniqueCounter);

    var post = new XMLHttpRequest();
    post.open("POST",url,async);
    post.setRequestHeader("Feed-URL",document.URL);
    return post;
}

function sendSyndicationCmd( cmd, arg, reloadArticles, async )
{
    if( reloadArticles == undefined ) reloadArticles = false;
    if( sSendCommands || reloadArticles ) {
        var post = requestForSyndicationCmd(cmd,arg,async);
        if( reloadArticles ) {

            post.setRequestHeader("Return-Articles","true");
            post.setRequestHeader("Filter-String", sIsPaginated && sFilterString ? sFilterString : "");
            post.onload = function( ) {reloadArticlesFromHTML(post.responseText);}
        }
        post.send(null);
//console.log("sent cmd "+cmd+" reload "+reloadArticles);
    }
}


//// SEPARATORS

function isArticleDiv(div) {
	return (div.className == sUnreadClass || div.className == sReadClass);
}


function getPreviousArticleDiv( div ) {
	while (div = div.previousSibling) { if (isArticleDiv(div)) break; }
	return div;
}


function getNextArticleDiv( div ) {
	while (div = div.nextSibling) { if (isArticleDiv(div)) break; }
	return div
}


function getNextReadArticleDiv( div ) {
	while (div = div.nextSibling) { if (div.className == sReadClass) break; }
	return div	
}


function showHideSeparator(articleDivWithSeparator, nextArticleDiv) {
	if (!articleDivWithSeparator) return;

	var isRead   = (articleDivWithSeparator.className == sReadClass);
	var nextRead = (!nextArticleDiv || nextArticleDiv.className == sReadClass);

	if (isRead && nextRead) {
		articleDivWithSeparator.setAttribute("showSeparator", "true");
	} else {
		articleDivWithSeparator.removeAttribute("showSeparator");
	}
}


//// MARK AS READ

function handleArticleClick( articleDiv )
{
	markArticlesRead( [ articleDiv ] );    	

	sUnreadCount--;
	redisplayCounts();

	// Only send synchronously if we are about to leave the page.  If the user just clicked on the div, go async
	var sourceElement = window.event.srcElement;
	var areWeLeavingPage = (sourceElement.nodeName.toUpperCase() == "A") && (sourceElement.href != undefined);
	var sendAsynchronously = (areWeLeavingPage == false);

	if (sClickToMarkRead) {
		sendSyndicationCmd("markRead", articleDiv.getAttribute("articleid"), false, sendAsynchronously);
	}

    window.event.cancelBubble = true; // Prevent parent element from handling click
    return true;
}


function markAllRead( )
{
    // Scan DOM for visible unread articles; make each display as read
	var articleDivs = new Array();
	var articleIds  = new Array();
    var parent = document.getElementById(sContentId);
    var children = parent.childNodes;

	var n = children.length;
    for( var i = 0; i < n; i++ ) {
        var articleDiv = children[i];

        if( articleDiv.nodeType == 1 && articleDiv.className == sUnreadClass ) {
            articleDivs.push(articleDiv);
			articleIds.push(articleDiv.getAttribute("articleid"));
        }
    }

	markArticlesRead(articleDivs);

	sUnreadCount = 0;
	redisplayCounts();

	sendSyndicationCmd("markAllRead", "", false);
}


function markArticlesRead( articleDivs ) {
	for( var i in articleDivs ) {
		articleDiv = articleDivs[i]
		articleDiv.className = sReadClass;
		
		var previousArticleDiv = getPreviousArticleDiv(articleDiv);
		var nextArticleDiv = getNextArticleDiv(articleDiv);

		showHideSeparator(previousArticleDiv, articleDiv);
		showHideSeparator(articleDiv, nextArticleDiv);	
	}
}


//// FILTERING

var sFilterField;
var sPrefiltered;                   // true if content has been prefiltered
var sFilterString;                  // current filter string. Do not set directly; call setFilterString
var sFilterMatcher;                 // WordMatcher for words in sFilterString.  Do not set directly; call setFilterString
var sFilterWords;                   // Fallback to JavsScript word matcher until native implementations ready
var sUsesBasicFiltering;            // Set to true if we're going to filter in JS entirely, versus using one of our native implementations
var sFirstDate, sLastDate;          // date range currently being shown (these are STRINGS)
var sSourceFilter;                  // index of source being displayed, null for all (STRINGS)
var sSourceUUID;

function setFilterString( filterString ) {
    sUsesBasicFiltering = true;
    sFilterString = sFilterMatcher = null;
    sFilterWords = new Array();
    if( filterString ) {
        filterString = filterString.toLocaleLowerCase();
        var words = (filterString+" ").match(/(\S+)(?=\s)/g);
        if( words ) {
            try {
                // First try creating a PubSubWordMatcher object
                sFilterMatcher = new PubSubWordMatcher(words);
                sUsesBasicFiltering = false;
            } catch (e) {
                // If that fails, we need to use the other types of word matching available to us
            }
            if( !sFilterMatcher ) {
                if( window.pubsub ) {
                    // If the window.pubsub object is available, use it
                    sFilterWords = window.pubsub.createWordMatcher(words);
                    sUsesBasicFiltering = false;
                } else {
                    // Otherwise, fall back to basic filtering, done directly in this Javascript
                    sFilterWords = words;
                }
            }
            sFilterString = filterString;
        }
    }
}

function setupFilter( )
{
    sFilterField = document.getElementById(sSearchfieldId);
    sFilterField.focus();
    setFilterString(sFilterField.value);
    sPrefiltered = (sFilterString != null);
}

function setURLToBookmark( )
{
    var protocol = document.location.protocol.toLowerCase();
    var url = document.location.href;
    if( protocol == "feeds:" ) {
        var trim = url.indexOf("&filter:");
        if( trim >= 0 )
            url = url.substring(0,trim);
        if( sFilterString )
            url += "&filter:"+encodeURIComponent(sFilterString);
    } else if( protocol = "feed:" ) {
        // Add a filter to a feed: URL by converting it into a single-item feeds:
        if( sFilterString ) {
            url = url.substring(5);
            if( url.indexOf("//")==0 )
                url = url.substring(2);            
            url = "feeds:"+encodeURIComponent(document.title)+"&"
                + url.replace("&","%26")
                + "&filter:"+encodeURIComponent(sFilterString);
        }
    }
    
    // Shove this URL into an attr of a  tag.
    // Do not change the id, or the attribute name, without a commensurate change in the browser.
    document.getElementById(sInfoId).setAttribute("URLToBookmark", url);
}

/** This is called by an event handler whenever the search field's content changes */
function setContentFilter( filter, bookmarkLinkTitle, searchLinkTitle )
{
    //var time = (new Date()).getTime();
    
    var oldFilter = sFilterString;
    setFilterString(filter);
    var reload = (sFilterString != oldFilter) && (sPrefiltered || sIsPaginated);

    setURLToBookmark();
    
    if( reload ) {
        // If articles were pre-filtered, we need to load all the articles:
        sPrefiltered = false;
        sResetPagination = true;
        sendSyndicationCmd("refilter",sFilterString,true);
    } else {
//console.log("setContentFilter->refilterArticles()");
        refilterArticles();
    }

    // Show/hide the bookmark link, as appropriate
    var bookmarkLinkDiv = document.getElementById(sBookmarkLinkId);
    var bookmarkLink = document.getElementById(sBookmarkHrefId);   
    if( ! sFilterString ) {
        var bookmarkAttr = bookmarkLinkDiv.getAttribute("class");
        
        // If we've just cleared the filter, and we're already bookmarked, hide the link
        if (bookmarkAttr == sBookmarkedClass) {
            bookmarkLinkDiv.style.display = "none";
        }
        // Otherwise, show Add Bookmark
        else {
            bookmarkLink.innerText = bookmarkLinkTitle;
            bookmarkLinkDiv.style.display = "block";
        }
    } else {
        // If we're adding chars to the filter, show the link with the text "Bookmark This Search"
        bookmarkLink.innerText = searchLinkTitle;
        bookmarkLinkDiv.style.display = "block";
    }
    
    //console.log("Filtering took "+((new Date()).getTime()-time)+" ms");
}


function subscribeInTunes()
{
    var href         = location.href;
    var indexOfColon = href.indexOf(":");

    location.href = "pcast" + href.substr(indexOfColon);    
}

function showAlertBanner(url, alert)
{
    document.getElementById(sAlertUrlId).setAttribute('value', url);
    document.getElementById(sAlertTextId).innerText = alert;
    document.getElementById(sAlertBannerId).style.display = "block";
}

function showAlertPasswordForm()
{
    // The form for entering credentials starts out hidden, and will be displayed
    // if appropriate for the type of error
    document.getElementById(sAlertPasswordFormId).style.display = "block";
}

function showTunesLink()
{
    // Only show the Subscribe in iTunes link if we are displaying a singleton feed
    var protocol = document.location.protocol.toLowerCase();
    if( protocol == "feed:" ) {
        var iTunesLink = document.getElementById(sTunesLinkId);
        iTunesLink.style.display = "block";
    }
}


function refreshFeed()
{
    //! It would be nicer to just ask the agent to check the feeds and send back any new articles
    // and insert them into the DOM. But it's too late for changes like that for Leopard. --jpa 8/2007
    window.location.reload();
}


var sTimespanDivs;


function currentTimespan( )
{
    if( ! sTimespanDivs )
        sTimespanDivs = document.getElementById(sTimespansId).getElementsByTagName("div");
    for( var i=0; i sMaxLastDate)));
    //console.log("setDateFilter " + index + " sMinFirstDate " + valOrNull(sMinFirstDate) + " sMaxLastDate " + valOrNull(sMaxLastDate) + " firstDate " + valOrNull(firstDate) + " lastDate " + valOrNull(lastDate));
    //if( reload ) console.log("Must reload HTML");
    sFirstDate = firstDate;
    sLastDate = lastDate;
    
    // Tell the back-end to update the prefs for this feed:
    if( sIsPaginated ) sResetPagination = true;
    sendSyndicationCmd("setTimespan",index,reload);
    
    if( reload ) {
        sMinFirstDate = firstDate;
        sMaxLastDate = lastDate;
    } else {
        // Now refilter, if we already have the HTML:
        refilterArticles();
    }
}


var sSourceDivs;


function currentSourceFilter( ) // returns uuid
{
    if( ! sSourceDivs )
        sSourceDivs = document.getElementById(sSourcelistId).getElementsByTagName("div");
    for( var i=0; i= sLastDate )
                return false;
            if( sSourceUUID == "" && sSourceFilter && sSourceFilter != article.getAttribute("sourceindex") )
                return false;
            if( ! articleMatchesFilter(article) )
                return false;
        }
        sTotalArticleCount++;
        if( article.className == sUnreadClass )
            sUnreadCount++;
    }
    return true;
}

var sTotalCountSpan, sUnreadCountSpan, sUnreadCountWrapperSpan;

function updateNoArticlesMessage( )
{
    // Show the "No Articles" message if there are no visible articles:
    var noarticles = document.getElementById(sNoArticlesId);

    // This can be executed before the page has finished loading, and this element isn't present yet
    if( noarticles ) {
        if( sTotalArticleCount == 0 ) {
            // Look up message to display, from attr of selected timespan div:
            var message = null;
            var divs = document.getElementById(sTimespansId).getElementsByTagName("div");
            for( var i=0; i0 ?"inline" :"none");
    
    if( sUnreadCountSpan == undefined )
        sUnreadCountSpan = document.getElementById(sUnreadCountId);
    sUnreadCountSpan.innerText = "" + sUnreadCount;
    
    updateNoArticlesMessage();
}

// Used for non-paged aggregates to recalculate filter in JavaScript space
function refilterArticles( )
{
    sTotalArticleCount = sUnreadCount = 0;
    var articles = document.getElementById(sContentId).childNodes;
    var articleToKeepInView;
//console.log("refilterArticles() filtering "+articles.length);
    var divsEvaluated = 0;
    for( var i=0; i