/** * Twitter - http://twitter.com * Copyright (C) 2010 Twitter * Author: Dustin Diaz (dustin@twitter.com) * * V 2.2.5 Twitter search/profile/faves/list widget * http://twitter.com/widgets * For full documented source see http://twitter.com/javascripts/widgets/widget.js * Hosting and modifications of the original source IS allowed. * * Example usage: * */ /** * @namespace TWTR public namespace for Twitter widget */ TWTR = window.TWTR || {}; /** * add core functionality to JS * Sugar Arrays http://www.dustindiaz.com/basement/sugar-arrays.html */ if (!Array.forEach) { Array.prototype.filter = function(fn, thisObj) { var scope = thisObj || window; var a = []; for (var i=0, j=this.length; i < j; ++i) { if (!fn.call(scope, this[i], i, this)) { continue; } a.push(this[i]); } return a; }; // sorta like inArray if used clever-like Array.prototype.indexOf = function(el, start) { var start = start || 0; for (var i=0; i < this.length; ++i) { if (this[i] === el) { return i; } } return -1; }; } /* first, a few dependencies */ (function() { if (TWTR && TWTR.Widget) { // this is most likely to happen when people try to embed multiple // widgets on the same page and include this script again return; } /** * Basic Array methods */ function each(a, fn, opt_scope) { for (var i=0, j=a.length; i < j; ++i) { fn.call(opt_scope || window, a[i], i, a); } } /** * Generic Animation utility to tween dom elements * * Copyright (c) 2009 Dustin Diaz & Twitter (http://www.dustindiaz.com) * MIT License */ /** * @constructor Animate * @param {HTMLElement} el the element we want to animate * @param {String} prop the CSS property we will be animating * @param {Object} opts a configuration object * object properties include * from {Int} * to {Int} * time {Int} time in milliseconds * callback {Function} */ function Animate(el, prop, opts) { this.el = el; this.prop = prop; this.from = opts.from; this.to = opts.to; this.time = opts.time; this.callback = opts.callback; this.animDiff = this.to - this.from; } /** * @static * @boolean * allows us to check if native CSS transitions are possible */ Animate.canTransition = function() { var el = document.createElement('twitter'); el.style.cssText = '-webkit-transition: all .5s linear;'; return !!el.style.webkitTransitionProperty; }(); /** * @private * @param {String} val the CSS value we will set on the property */ Animate.prototype._setStyle = function(val) { switch (this.prop) { case 'opacity': this.el.style[this.prop] = val; this.el.style.filter = 'alpha(opacity=' + val * 100 + ')'; break; default: this.el.style[this.prop] = val + 'px'; break; } }; /** * @private * this is the tweening function */ Animate.prototype._animate = function() { var that = this; this.now = new Date(); this.diff = this.now - this.startTime; if (this.diff > this.time) { this._setStyle(this.to); if (this.callback) { this.callback.call(this); } clearInterval(this.timer); return; } this.percentage = (Math.floor((this.diff / this.time) * 100) / 100); this.val = (this.animDiff * this.percentage) + this.from; this._setStyle(this.val); }; /** * @public * begins the animation */ Animate.prototype.start = function() { var that = this; this.startTime = new Date(); this.timer = setInterval(function() { that._animate.call(that); }, 15); }; /** * @constructor * Widget Base for new instances of the Twitter search widget * @param {Object} opts the configuration options for the widget */ TWTR.Widget = function(opts) { this.init(opts); }; (function() { // Internal Namespace. var twttr = {}; var isHttps = location.protocol.match(/https/); var httpsImageRegex = /^.+\/profile_images/; var httpsImageReplace = 'https://s3.amazonaws.com/twitter_production/profile_images'; var matchUrlScheme = function(url) { return isHttps ? url.replace(httpsImageRegex, httpsImageReplace) : url; }; // cache object for searching duplicates var reClassNameCache = {}; // reusable regex for searching classnames var getClassRegEx = function(c) { // check to see if regular expression already exists var re = reClassNameCache[c]; if (!re) { re = new RegExp('(?:^|\\s+)' + c + '(?:\\s+|$)'); reClassNameCache[c] = re; } return re; }; var getByClass = function(c, tag, root, apply) { var tag = tag || '*'; var root = root || document; var nodes = [], elements = root.getElementsByTagName(tag), re = getClassRegEx(c); for (var i = 0, len = elements.length; i < len; ++i) { if (re.test(elements[i].className)) { nodes[nodes.length] = elements[i]; if (apply) { apply.call(elements[i], elements[i]); } } } return nodes; }; var browser = function() { var ua = navigator.userAgent; return { ie: ua.match(/MSIE\s([^;]*)/) }; }(); var byId = function(id) { if (typeof id == 'string') { return document.getElementById(id); } return id; }; var trim = function(str) { return str.replace(/^\s+|\s+$/g, ''); }; var getViewportHeight = function() { var height = self.innerHeight; // Safari, Opera var mode = document.compatMode; if ((mode || browser.ie)) { // IE, Gecko height = (mode == 'CSS1Compat') ? document.documentElement.clientHeight : // Standards document.body.clientHeight; // Quirks } return height; }; var getTarget = function(e, resolveTextNode) { var target = e.target || e.srcElement; return resolveTextNode(target); }; var resolveTextNode = function(el) { try { if (el && 3 == el.nodeType) { return el.parentNode; } else { return el; } } catch (ex) { } }; var getRelatedTarget = function(e) { var target = e.relatedTarget; if (!target) { if (e.type == 'mouseout') { target = e.toElement; } else if (e.type == 'mouseover') { target = e.fromElement; } } return resolveTextNode(target); }; var insertAfter = function(el, reference) { reference.parentNode.insertBefore(el, reference.nextSibling); }; var removeElement = function(el) { try { el.parentNode.removeChild(el); } catch (ex) { } }; var getFirst = function(el) { return el.firstChild; }; var withinElement = function(e) { var parent = getRelatedTarget(e); while (parent && parent != this) { try { parent = parent.parentNode; } catch(ex) { parent = this; } } if (parent != this) { return true; } return false; }; var getStyle = function() { if (document.defaultView && document.defaultView.getComputedStyle) { return function(el, property) { var value = null; var computed = document.defaultView.getComputedStyle(el, ''); if (computed) { value = computed[property]; } var ret = el.style[property] || value; return ret; }; } else if (document.documentElement.currentStyle && browser.ie) { // IE method return function(el, property) { var value = el.currentStyle ? el.currentStyle[property] : null; return (el.style[property] || value); }; } }(); /** * classes object * - has - add - remove */ var classes = { has: function(el, c) { return new RegExp("(^|\\s)" + c + "(\\s|$)").test(byId(el).className); }, add: function(el, c) { if (!this.has(el, c)) { byId(el).className = trim(byId(el).className) + ' ' + c; } }, remove: function(el, c) { if (this.has(el, c)) { byId(el).className = byId(el).className.replace(new RegExp("(^|\\s)" + c + "(\\s|$)", "g"), ""); } } }; /** * basic x-browser event listener util * eg: events.add(element, 'click', fn); */ var events = { add: function(el, type, fn) { if (el.addEventListener) { el.addEventListener(type, fn, false); } else { el.attachEvent('on' + type, function() { fn.call(el, window.event); }); } }, remove: function(el, type, fn) { if (el.removeEventListener) { el.removeEventListener(type, fn, false); } else { el.detachEvent('on' + type, fn); } } }; var hex_rgb = function() { function HexToR(h) { return parseInt((h).substring(0,2),16); } function HexToG(h) { return parseInt((h).substring(2,4),16); } function HexToB(h) { return parseInt((h).substring(4,6),16); } return function(hex) { return [HexToR(hex), HexToG(hex), HexToB(hex)]; }; }(); /** * core type detection on javascript objects */ var is = { bool: function(b) { return typeof b === 'boolean'; }, def: function(o) { return !(typeof o === 'undefined'); }, number: function(n) { return typeof n === 'number' && isFinite(n); }, string: function(s) { return typeof s === 'string'; }, fn: function(f) { return typeof f === 'function'; }, array: function(a) { if (a) { return is.number(a.length) && is.fn(a.splice); } return false; } }; var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; var absoluteTime = function(s) { var d = new Date(s); if (browser.ie) { d = Date.parse(s.replace(/( \+)/, ' UTC$1')); } var ampm = ''; var hour = function() { var h = d.getHours(); if (h > 0 && h < 13) { ampm = 'am'; return h; } else if (h < 1) { ampm = 'am'; return 12; } else { ampm = 'pm'; return h - 12; } }(); var minutes = d.getMinutes(); var seconds = d.getSeconds(); function getRest() { var today = new Date(); if (today.getDate() != d.getDate() || today.getYear() != d.getYear() || today.getMonth() != d.getMonth()) { return ' - ' + months[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear(); } else { return ''; } } return hour + ':' + minutes + ampm + getRest(); }; /** * relative time calculator * @param {string} twitter date string returned from Twitter API * @return {string} relative time like "2 minutes ago" */ var timeAgo = function(dateString) { var rightNow = new Date(); var then = new Date(dateString); if (browser.ie) { // IE can't parse these crazy Ruby dates then = Date.parse(dateString.replace(/( \+)/, ' UTC$1')); } var diff = rightNow - then; var second = 1000, minute = second * 60, hour = minute * 60, day = hour * 24, week = day * 7; if (isNaN(diff) || diff < 0) { return ""; // return blank string if unknown } if (diff < second * 2) { // within 2 seconds return "right now"; } if (diff < minute) { return Math.floor(diff / second) + " seconds ago"; } if (diff < minute * 2) { return "about 1 minute ago"; } if (diff < hour) { return Math.floor(diff / minute) + " minutes ago"; } if (diff < hour * 2) { return "about 1 hour ago"; } if (diff < day) { return Math.floor(diff / hour) + " hours ago"; } if (diff > day && diff < day * 2) { return "yesterday"; } if (diff < day * 365) { return Math.floor(diff / day) + " days ago"; } else { return "over a year ago"; } }; /** * The Twitalinkahashifyer! * http://www.dustindiaz.com/basement/ify.html * Eg: * ify.clean('your tweet text'); */ var ify = { link: function(tweet) { return tweet.replace(/\b(((https*\:\/\/)|www\.)[^\"\']+?)(([!?,.\)]+)?(\s|$))/g, function(link, m1, m2, m3, m4) { var http = m2.match(/w/) ? 'http://' : ''; return '' + ((m1.length > 25) ? m1.substr(0, 24) + '...' : m1) + '' + m4; }); }, at: function(tweet) { return tweet.replace(/\B[@@]([a-zA-Z0-9_]{1,20})/g, function(m, username) { return '@' + username + ''; }); }, list: function(tweet) { return tweet.replace(/\B[@@]([a-zA-Z0-9_]{1,20}\/\w+)/g, function(m, userlist) { return '@' + userlist + ''; }); }, hash: function(tweet) { return tweet.replace(/(^|\s+)#(\w+)/gi, function(m, before, hash) { return before + '#' + hash + ''; }); }, clean: function(tweet) { return this.hash(this.at(this.list(this.link(tweet)))); } }; /** * @constructor the classic twitter occasional job * @param {Function} job The job to execute upon each request * @param {Function} decay The deciding boolean method on whether to decay * @param {Int} interval The number in milliseconds to wait before executing */ function Occasionally(job, decayFn, interval) { this.job = job; this.decayFn = decayFn; this.interval = interval; this.decayRate = 1; this.decayMultiplier = 1.25; this.maxDecayTime = 3 * 60 * 1000; // 3 minutes } Occasionally.prototype = { /** * @public * @return self * starts our occasional job */ start: function() { this.stop().run(); return this; }, /** * @public * @return self * stops the occasional job */ stop: function() { if (this.worker) { window.clearTimeout(this.worker); } return this; }, /** * @private */ run: function() { var that = this; this.job(function() { // running our decayer callback that.decayRate = that.decayFn() ? Math.max(1, that.decayRate / that.decayMultiplier) : that.decayRate * that.decayMultiplier; var expire = that.interval * that.decayRate; expire = (expire >= that.maxDecayTime) ? that.maxDecayTime : expire; expire = Math.floor(expire); that.worker = window.setTimeout( function () { that.run.call(that); }, expire ); }); }, /** * @public * @return self * stops occasional job and resets object */ destroy: function() { this.stop(); this.decayRate = 1; return this; } }; /** * @Constructor runs a timer on an array passing back * the next needle on each interval * @param haystack {Array} * @param time {Int} time in ms * @param loop {Bool} does this continue forever? * @param callback {Function} method that is passed back a needle for each interval */ function IntervalJob(time, loop, callback) { this.time = time || 6000; this.loop = loop || false; this.repeated = 0; this.callback = callback; this.haystack = []; } IntervalJob.prototype = { set: function(haystack) { this.haystack = haystack; }, add: function(needle) { this.haystack.unshift(needle); }, /** * @public * @return self * begins the interval job */ start: function() { if (this.timer) { return this; } this._job(); var that = this; this.timer = setInterval( function() { that._job.call(that); }, this.time ); return this; }, /** * @public * @return self * stops the interval */ stop: function() { if (this.timer) { window.clearInterval(this.timer); this.timer = null; } return this; }, /** * @private */ _next: function() { var old = this.haystack.shift(); if (old && this.loop) { this.haystack.push(old); } return old || null; }, /** * @private */ _job: function() { var next = this._next(); if (next) { this.callback(next); } return this; } }; function Tweet(tweet) { function showPopular() { if (tweet.needle.metadata && tweet.needle.metadata.result_type && tweet.needle.metadata.result_type == 'popular') { return '' + tweet.needle.metadata.recent_retweets + '+ recent retweets'; } else { return ''; } } var html = '
\
\
' + tweet.user + ' profile
\
\
\

\ ' + tweet.user + ' ' + tweet.tweet + ' \ \ ' + tweet.created_at + ' ·\ reply · \ retweet · \ favorite \ ' + showPopular() + ' \

\
\
'; var div = document.createElement('div'); div.id = 'tweet-id-' + ++Tweet._tweetCount; div.className = 'twtr-tweet'; div.innerHTML = html; this.element = div; } // static count so all tweets (even on multiple inst widgets) will have unique ids Tweet._tweetCount = 0; twttr.loadStyleSheet = function(url, widgetEl) { if (!TWTR.Widget.loadingStyleSheet) { TWTR.Widget.loadingStyleSheet = true; var linkElement = document.createElement('link'); linkElement.href = url; linkElement.rel = 'stylesheet'; linkElement.type = 'text/css'; document.getElementsByTagName('head')[0].appendChild(linkElement); var timer = setInterval(function() { var style = getStyle(widgetEl, 'position'); if (style == 'relative') { clearInterval(timer); timer = null; TWTR.Widget.hasLoadedStyleSheet = true; } }, 50); } }; (function() { var isLoaded = false; twttr.css = function(rules) { var styleElement = document.createElement('style'); styleElement.type = 'text/css'; if (browser.ie) { styleElement.styleSheet.cssText = rules; } else { var frag = document.createDocumentFragment(); frag.appendChild(document.createTextNode(rules)); styleElement.appendChild(frag); } function append() { document.getElementsByTagName('head')[0].appendChild(styleElement); } // oh IE we love you. // this is needed because you can't modify document body when page is loading if (!browser.ie || isLoaded) { append(); } else { window.attachEvent('onload', function() { isLoaded = true; append(); }); } }; })(); TWTR.Widget.isLoaded = false; TWTR.Widget.loadingStyleSheet = false; TWTR.Widget.hasLoadedStyleSheet = false; TWTR.Widget.WIDGET_NUMBER = 0; TWTR.Widget.matches = { mentions: /^@[a-zA-Z0-9_]{1,20}\b/, any_mentions: /\b@[a-zA-Z0-9_]{1,20}\b/ }; TWTR.Widget.jsonP = function(url, callback) { var script = document.createElement('script'); var head = document.getElementsByTagName('head')[0]; script.type = 'text/javascript'; script.src = url; head.insertBefore(script, head.firstChild); callback(script); return script; }; TWTR.Widget.prototype = function() { var http = isHttps ? 'https://' : 'http://'; var domain = window.location.hostname.match(/twitter\.com/) ? (window.location.hostname + ":" + window.location.port) : 'twitter.com'; var base = http + 'search.' + domain + '/search.'; var profileBase = http + 'api.' + domain + '/1/statuses/user_timeline.'; var favBase = http + domain + '/favorites/'; var listBase = http + 'api.' + domain + '/1/'; var occasionalInterval = 25000; // 25 seconds var defaultAvatar = isHttps ? 'https://twitter-widgets.s3.amazonaws.com/j/1/default.gif' : 'http://widgets.twimg.com/j/1/default.gif'; return { init: function(opts) { var that = this; // first, define public callback for this widget this._widgetNumber = ++TWTR.Widget.WIDGET_NUMBER; TWTR.Widget['receiveCallback_' + this._widgetNumber] = function(resp) { that._prePlay.call(that, resp); }; this._cb = 'TWTR.Widget.receiveCallback_' + this._widgetNumber; this.opts = opts; this._base = base; this._isRunning = false; this._hasOfficiallyStarted = false; this._hasNewSearchResults = false; this._rendered = false; this._profileImage = false; this._isCreator = !!opts.creator; this._setWidgetType(opts.type); this.timesRequested = 0; this.runOnce = false; this.newResults = false; this.results = []; this.jsonMaxRequestTimeOut = 19000; this.showedResults = []; this.sinceId = 1; this.source = 'TWITTERINC_WIDGET'; this.id = opts.id || 'twtr-widget-' + this._widgetNumber; this.tweets = 0; this.setDimensions(opts.width, opts.height); this.interval = opts.interval || 6000; this.format = 'json'; this.rpp = opts.rpp || 50; this.subject = opts.subject || ''; this.title = opts.title || ''; this.setFooterText(opts.footer); this.setSearch(opts.search); this._setUrl(); this.theme = opts.theme ? opts.theme : this._getDefaultTheme(); if (!opts.id) { document.write('
'); } this.widgetEl = byId(this.id); if (opts.id) { classes.add(this.widgetEl, 'twtr-widget'); } // if (opts.version >= 2 && !TWTR.Widget.hasLoadedStyleSheet) { // if (isHttps) { // twttr.loadStyleSheet('https://twitter-widgets.s3.amazonaws.com/j/2/widget.css', this.widgetEl); // } else if (opts.creator) { // twttr.loadStyleSheet('/stylesheets/widgets/widget.css', this.widgetEl); // } else { // twttr.loadStyleSheet('http://localhost.twitter.com:3000/stylesheets/widgets/widget.css', this.widgetEl); // twttr.loadStyleSheet('http://widgets.twimg.com/j/2/widget.css', this.widgetEl); // } // } if (opts.version >= 2 && !TWTR.Widget.hasLoadedStyleSheet) { if (isHttps) { twttr.loadStyleSheet(templateDir+ '/widgets/widget-twitter/twitter-widget.css', this.widgetEl); } else if (opts.creator) { twttr.loadStyleSheet(templateDir+ '/widgets/widget-twitter/twitter-widget.css', this.widgetEl); } else { // twttr.loadStyleSheet('http://localhost.twitter.com:3000/stylesheets/widgets/widget.css', this.widgetEl); twttr.loadStyleSheet(templateDir+ '/widgets/widget-twitter/twitter-widget.css', this.widgetEl); } } this.occasionalJob = new Occasionally( function(decay) { that.decay = decay; that._getResults.call(that); }, function() { return that._decayDecider.call(that); }, occasionalInterval ); this._ready = is.fn(opts.ready) ? opts.ready : function() { }; // preset features this._isRelativeTime = true; this._tweetFilter = false; this._avatars = true; this._isFullScreen = false; this._isLive = true; this._isScroll = false; this._loop = true; this._showTopTweets = (this._isSearchWidget) ? true : false; this._behavior = 'default'; this.setFeatures(this.opts.features); this.intervalJob = new IntervalJob(this.interval, this._loop, function(tweet) { that._normalizeTweet(tweet); }); return this; }, /** * @public * @param {Int} w - width for widget * @param {Int} h - height for widget * @return self */ setDimensions: function(w, h) { this.wh = (w && h) ? [w, h] : [250, 300]; // default w/h if none provided if (w == 'auto' || w == '100%') { this.wh[0] = '100%'; } else { this.wh[0] = ((this.wh[0] < 150) ? 150 : this.wh[0]) + 'px'; // min width is 150 } this.wh[1] = ((this.wh[1] < 100) ? 100 : this.wh[1]) + 'px'; // min height is 100 return this; }, setRpp: function(rpp) { var rpp = parseInt(rpp); this.rpp = (is.number(rpp) && (rpp > 0 && rpp <= 100)) ? rpp : 30; return this; }, /** * @private * @param {String} type the kind of widget you're instantiating * @return self */ _setWidgetType: function(type) { this._isSearchWidget = false; this._isProfileWidget = false; this._isFavsWidget = false; this._isListWidget = false; switch(type) { case 'profile': this._isProfileWidget = true; break; case 'search': this._isSearchWidget = true; this.search = this.opts.search; break; case 'faves': case 'favs': this._isFavsWidget = true; break; case 'list': case 'lists': this._isListWidget = true; break; } return this; }, /** * @public * @param {object} * @return self * allows implementer to set features which include: * - avatars {bool} * - timestamp {bool} * - hashtags {bool} * setting any of the previous properties will appropriately hide/show that feature * @example * WidgetInstance.setFeatures({ fullscreen: true, avatars: true, timestamp: false, hashtags: false }).render().start(); * @return self */ setFeatures: function(features) { if (features) { if (is.def(features.filters)) { this._tweetFilter = features.filters; } if (is.def(features.dateformat)) { this._isRelativeTime = !!(features.dateformat !== 'absolute'); } if (is.def(features.fullscreen) && is.bool(features.fullscreen)) { if (features.fullscreen) { this._isFullScreen = true; this.wh[0] = '100%'; this.wh[1] = (getViewportHeight() - 90) + 'px'; var that = this; events.add(window, 'resize', function(e) { that.wh[1] = getViewportHeight(); that._fullScreenResize(); }); } } if (is.def(features.loop) && is.bool(features.loop)) { this._loop = features.loop; } if (is.def(features.behavior) && is.string(features.behavior)) { switch (features.behavior) { case 'all': this._behavior = 'all'; break; case 'preloaded': this._behavior = 'preloaded'; break; default: this._behavior = 'default'; break; } } if (is.def(features.toptweets) && is.bool(features.toptweets)) { this._showTopTweets = features.toptweets; var showTopTweet = (this._showTopTweets) ? 'inline-block' : 'none'; twttr.css('#' + this.id + ' .twtr-popular { display: ' + showTopTweet + '; }'); } if (!is.def(features.toptweets)) { this._showTopTweets = true; var showTopTweet = (this._showTopTweets) ? 'inline-block' : 'none'; twttr.css('#' + this.id + ' .twtr-popular { display: ' + showTopTweet + '; }'); } if (is.def(features.avatars) && is.bool(features.avatars)) { if (!features.avatars) { twttr.css('#' + this.id + ' .twtr-avatar, #' + this.id + ' .twtr-user { display: none; } ' + '#' + this.id + ' .twtr-tweet-text { margin-left: 0; }'); this._avatars = false; } else { var margin = (this._isFullScreen) ? '90px' : '40px'; twttr.css('#' + this.id + ' .twtr-avatar { display: block; } #' + this.id + ' .twtr-user { display: inline; } ' + '#' + this.id + ' .twtr-tweet-text { margin-left: ' + margin + '; }'); this._avatars = true; } } else { if (this._isProfileWidget) { this.setFeatures({ avatars: false }); this._avatars = false; } else { this.setFeatures({ avatars: true }); this._avatars = true; } } if (is.def(features.hashtags) && is.bool(features.hashtags)) { (!features.hashtags) ? twttr.css('#' + this.id + ' a.twtr-hashtag { display: none; }') : ''; } if (is.def(features.timestamp) && is.bool(features.timestamp)) { var display = features.timestamp ? 'block' : 'none'; twttr.css('#' + this.id + ' em { display: ' + display + '; }'); } if (is.def(features.live) && is.bool(features.live)) { this._isLive = features.live; } if (is.def(features.scrollbar) && is.bool(features.scrollbar)) { this._isScroll = features.scrollbar; } } else { if (this._isProfileWidget) { this.setFeatures({ avatars: false }); this._avatars = false; } if (this._isProfileWidget || this._isFavsWidget) { this.setFeatures({ behavior: 'all' }); } } return this; }, /** * @private * @param e Event listener for window resizing */ _fullScreenResize: function() { var timeline = getByClass('twtr-timeline', 'div', document.body, function(el) { el.style.height = (getViewportHeight() - 90) + 'px'; }); }, /** * @public facade * @param {int} in seconds * convenience method for setting time between each tweet render * @return self */ setTweetInterval: function(interval) { this.interval = interval; return this; }, /** * @public * @param {string} url * sets a url base for the JSONP call * useful for future API implementations or moderation platforms * @return self */ setBase: function(b) { this._base = b; return this; }, /** * @public * @param {string} username * used to distinguish a "favs" widget * @return self */ setUser: function(username, opt_realname) { this.username = username; this.realname = opt_realname || ' '; if (this._isFavsWidget) { this.setBase(favBase + username + '.'); } else if (this._isProfileWidget) { this.setBase(profileBase + this.format + '?screen_name=' + username); } this.setSearch(' '); return this; }, /** * @public * @param {string} username - the owner of the list * @param {string} listName - the name of the list * return self */ setList: function(username, listname) { this.listslug = listname.replace(/ /g, '-').toLowerCase(); this.username = username; this.setBase(listBase + username + '/lists/' + this.listslug + '/statuses.'); this.setSearch(' '); return this; }, /** * @public * @param {string} * sets the profile image source to display in the widget * @return self */ setProfileImage: function(url) { this._profileImage = url; this.byClass('twtr-profile-img', 'img').src = matchUrlScheme(url); this.byClass('twtr-profile-img-anchor', 'a').href = 'http://twitter.com/intent/user?screen_name=' + this.username; return this; }, /** * @public * @param {string} * sets the main title to display at top of widget * @return self */ setTitle: function(title) { this.title = title; this.widgetEl.getElementsByTagName('h3')[0].innerHTML = this.title; return this; }, /** * @public * @param {string} * sets the main caption to display at top of widget (below title) * @return self */ setCaption: function(subject) { this.subject = subject; this.widgetEl.getElementsByTagName('h4')[0].innerHTML = this.subject; return this; }, /** * @public * @param {string} * sets the footer text * @return self */ setFooterText: function(s) { this.footerText = (is.def(s) && is.string(s)) ? s : 'More Tweets'; if (this._rendered) { this.byClass('twtr-join-conv', 'a').innerHTML = this.footerText; } return this; }, /** * @public * @param {string} * @return self * does double time. sets the search terms, and sets the appropriate * hyper reference on bottom anchor if widget has been rendered */ setSearch: function(s) { this.searchString = s || ''; this.search = encodeURIComponent(this.searchString); this._setUrl(); if (this._rendered) { var anchor = this.byClass('twtr-join-conv', 'a'); anchor.href = 'http://twitter.com/' + this._getWidgetPath(); } return this; }, _getWidgetPath: function() { if (this._isProfileWidget) { return this.username; } else if (this._isFavsWidget) { return this.username + '/favorites'; } else if (this._isListWidget) { return this.username + '/lists/' + this.listslug; } else { return '#search?q=' + this.search; } }, /** * @private * @return self * creates the proper URL to request data via JSONP */ _setUrl: function() { var that = this; function cacheBust() { // chrome i hate your caching return '&' + (+new Date) + '=cachebust'; } function showSince() { return (that.sinceId == 1) ? '' : '&since_id=' + that.sinceId + '&refresh=true'; } if (this._isProfileWidget) { this.url = this._base + '&callback=' + this._cb + '&include_rts=true' + '&count=' + this.rpp + showSince() + '&clientsource=' + this.source; } else if (this._isFavsWidget || this._isListWidget) { this.url = this._base + this.format + '?callback=' + this._cb + showSince() + '&include_rts=true' + '&clientsource=' + this.source; } else { this.url = this._base + this.format + '?q=' + this.search + '&include_rts=true' + '&callback=' + this._cb + '&rpp=' + this.rpp + showSince() + '&clientsource=' + this.source; if (!this.runOnce) { this.url += '&result_type=mixed'; } } this.url += cacheBust(); return this; }, /** * @private */ _getRGB: function(hex) { return hex_rgb(hex.substring(1, 7)); }, /** * @public * @param {object} * @param {boolean} important whether to be important style * @return self * allows implementer to set their own theme. * theme object can be passed into contructor, or set here. * defaults to default theme properties when not set */ setTheme: function(o, important) { var that = this; var imp = ' !important'; var onCreator = ((window.location.hostname.match(/twitter\.com/)) && (window.location.pathname.match(/goodies/))); if (important || onCreator) { imp = ''; } this.theme = { shell: { background: function() { return o.shell.background || that._getDefaultTheme().shell.background; }(), color: function() { return o.shell.color || that._getDefaultTheme().shell.color; }() }, tweets: { background: function() { return o.tweets.background || that._getDefaultTheme().tweets.background; }(), color: function() { return o.tweets.color || that._getDefaultTheme().tweets.color; }(), links: function() { return o.tweets.links || that._getDefaultTheme().tweets.links; }() } }; var style = '#' + this.id + ' .twtr-doc, \ #' + this.id + ' .twtr-hd a, \ #' + this.id + ' h3, \ #' + this.id + ' h4, \ #' + this.id + ' .twtr-popular {\ }\ #' + this.id + ' .twtr-popular {\ }\ #' + this.id + ' .twtr-tweet a {\ }\ #' + this.id + ' .twtr-bd, #' + this.id + ' .twtr-timeline i a, \ #' + this.id + ' .twtr-bd p {\ }\ #' + this.id + ' .twtr-new-results, \ #' + this.id + ' .twtr-results-inner, \ #' + this.id + ' .twtr-timeline {\ }'; if (browser.ie) { style += '#' + this.id + ' .twtr-tweet { background: ' + this.theme.tweets.background + imp + '; }'; } twttr.css(style); return this; }, /** * @public * @param {string} classname * @param {string} tagname * @param optional {bool} whether to return collection or defaults to first match * @return HTML Element || Array HTML Elements * helper to get elements by classname based on the widget being the context */ byClass: function(c, tag, opt_all) { var collection = getByClass(c, tag, byId(this.id)); return (opt_all) ? collection : collection[0]; }, /** * @public * @return self * renders the widget onto an HTML page */ render: function() { var that = this; if (!TWTR.Widget.hasLoadedStyleSheet) { window.setTimeout(function() { that.render.call(that); }, 50); return this; } this.setTheme(this.theme, this._isCreator); if (this._isProfileWidget) { classes.add(this.widgetEl, 'twtr-widget-profile'); } if (this._isScroll) { classes.add(this.widgetEl, 'twtr-scroll') } if (!this._isLive && !this._isScroll) { this.wh[1] = 'auto'; } if (this._isSearchWidget && this._isFullScreen) { document.title = 'Twitter search: ' + escape(this.searchString); } this.widgetEl.innerHTML = this._getWidgetHtml(); var timeline = this.byClass('twtr-timeline', 'div'); if (this._isLive && !this._isFullScreen) { var over = function(e) { if (that._behavior === 'all') { return; } if (withinElement.call(this, e)) { that.pause.call(that); } }; var out = function(e) { if (that._behavior === 'all') { return; } if (withinElement.call(this, e)) { that.resume.call(that); } }; this.removeEvents = function() { events.remove(timeline, 'mouseover', over); events.remove(timeline, 'mouseout', out); }; events.add(timeline, 'mouseover', over); events.add(timeline, 'mouseout', out); } this._rendered = true; // call the ready handler this._ready(); return this; }, /** * empty placeholder for removing events * on live widgets */ removeEvents: function() { }, /** * @private * @return {object} theme */ _getDefaultTheme: function() { return { shell: { background: '#8ec1da', color: '#ffffff' }, tweets: { background: '#ffffff', color: '#444444', links: '#1985b5' } }; }, /** * @private * @return {string} * builds an HTML string that represents the widget chrome */ _getWidgetHtml: function() { var that = this; function getHeader() { if (that._isProfileWidget) { return 'profile\

\

'; } else { return '

' + that.title + '

' + that.subject + '

'; } } function isFull() { return that._isFullScreen ? ' twtr-fullscreen' : ''; } var logo = isHttps ? 'https://twitter-widgets.s3.amazonaws.com/i/widget-logo.png' : 'http://widgets.twimg.com/i/widget-logo.png'; if (this._isFullScreen) { logo = 'https://twitter-widgets.s3.amazonaws.com/i/widget-logo-fullscreen.png'; } var html = '\
\
\ ' + getHeader() + ' \
\
\
\
\
\ \
\
\
\
\
\ \ ' + this.footerText + '\
\
\
'; return html; }, /** * @private * @return self * puts the tweet in the dom */ _appendTweet: function(el) { this._insertNewResultsNumber(); insertAfter(el, this.byClass('twtr-reference-tweet', 'div')); return this; }, /** * @private * @return self * slides in a rendered tweet */ _slide: function(el) { var that = this; var height = getFirst(el).offsetHeight; if (this.runOnce) { new Animate(el, 'height', { from: 0, to: height, time: 500, callback: function() { that._fade.call(that, el); } }).start(); } return this; }, /** * @private * @return self * fades in a rendered tweet */ _fade: function(el) { var that = this; if (Animate.canTransition) { el.style.webkitTransition = 'opacity 0.5s ease-out'; el.style.opacity = 1; return this; } new Animate(el, 'opacity', { from: 0, to: 1, time: 500 }).start(); return this; }, /** * @private * @return self * removes the last tweet if it is offscreen */ _chop: function() { if (this._isScroll) { return this; } var tweets = this.byClass('twtr-tweet', 'div', true); var resultUpdates = this.byClass('twtr-new-results', 'div', true); if (tweets.length) { for (var i=tweets.length - 1; i >=0; i--) { var tweet = tweets[i]; var top = parseInt(tweet.offsetTop); if (top > parseInt(this.wh[1])) { removeElement(tweet); } else { break; } } if (resultUpdates.length > 0) { var result = resultUpdates[resultUpdates.length - 1]; var resultTop = parseInt(result.offsetTop); if (resultTop > parseInt(this.wh[1])) { removeElement(result); } } } return this; }, /** * @private * @return self * Big Facade for chop, append, slide, and fade */ _appendSlideFade: function(opt_element) { var el = opt_element || this.tweet.element; this ._chop() ._appendTweet(el) ._slide(el); return this; }, /** * @private * @return self * generates the HTML for a single tweet item */ _createTweet: function(o) { o.timestamp = o.created_at; o.created_at = this._isRelativeTime ? timeAgo(o.created_at) : absoluteTime(o.created_at); this.tweet = new Tweet(o); if (this._isLive && this.runOnce) { this.tweet.element.style.opacity = 0; this.tweet.element.style.filter = 'alpha(opacity:0)'; this.tweet.element.style.height = '0'; } return this; }, /** * @private * @param {Function} callback function that receives the results * makes a jsonP call to twitter.com */ _getResults: function() { var that = this; this.timesRequested++; this.jsonRequestRunning = true; this.jsonRequestTimer = window.setTimeout(function() { if (that.jsonRequestRunning) { clearTimeout(that.jsonRequestTimer); that.jsonRequestTimer = null; } that.jsonRequestRunning = false; removeElement(that.scriptElement); that.newResults = false; that.decay(); }, this.jsonMaxRequestTimeOut); TWTR.Widget.jsonP(that.url, function(script) { that.scriptElement = script; }); }, /** * @public * @return self * clears out the tweet space. used internally, * but free to use publicly */ clear: function() { var tweets = this.byClass('twtr-tweet', 'div', true); var results = this.byClass('twtr-new-results', 'div', true); tweets = tweets.concat(results); each(tweets, function(el) { removeElement(el); }); return this; }, _sortByMagic: function(results) { var that = this; if (this._tweetFilter) { if (this._tweetFilter.negatives) { results = results.filter(function(el) { if (!that._tweetFilter.negatives.test(el.text)) { return el; } }); } if (this._tweetFilter.positives) { results = results.filter(function(el) { if (that._tweetFilter.positives.test(el.text)) { return el; } }); } } switch (this._behavior) { case 'all': this._sortByLatest(results); break; case 'preloaded': default: this._sortByDefault(results); break; }; if (this._isLive && this._behavior !== 'all') { this.intervalJob.set(this.results); this.intervalJob.start(); } return this; }, /** * @private * @return results * puts the toptweets for search widget at the top */ _loadTopTweetsAtTop: function(results) { var regular = [], popular = [], arr = []; // top tweets each(results, function(el) { if (el.metadata && el.metadata.result_type && el.metadata.result_type == 'popular') { popular.push(el); } else { regular.push(el); } }); var result = popular.concat(regular); return result; }, _sortByLatest: function(results) { this.results = results; this.results = this.results.slice(0, this.rpp); this.results = this._loadTopTweetsAtTop(this.results); this.results.reverse(); return this; }, /** * @private * @return self * default sorting method which tracks views and loops */ _sortByDefault: function(results) { var that = this; var getDater = function(dateString) { return new Date(dateString).getTime(); }; // merge new results with old this.results.unshift.apply(this.results, results); each(this.results, function(el) { if (!el.views) { el.views = 0; } }); // sort by date this.results.sort(function(a, b) { if (getDater(a.created_at) > getDater(b.created_at)) { return -1; } else if (getDater(a.created_at) < getDater(b.created_at)) { return 1; } else { return 0; } }); // now cut off the oldest this.results = this.results.slice(0, this.rpp); this.results = this._loadTopTweetsAtTop(this.results); var foo = this.results; // now sort by views this.results = this.results.sort(function(a, b) { if (a.views < b.views) { return -1; } else if (a.views > b.views) { return 1; } return 0; }); if (!this._isLive) { this.results.reverse(); } }, /** * @private * @method prePlay does a pre-check against last result. * @param resp the JSON response from twitter JsonP API */ _prePlay: function(resp) { if (this.jsonRequestTimer) { clearTimeout(this.jsonRequestTimer); this.jsonRequestTimer = null; } if (!browser.ie) { removeElement(this.scriptElement); } if (resp.error) { this.newResults = false; } else if (resp.results && resp.results.length > 0) { this.response = resp; this.newResults = true; this.sinceId = resp.max_id_str; this._sortByMagic(resp.results); if (this.isRunning()) { this._play(); } } else if ((this._isProfileWidget || this._isFavsWidget || this._isListWidget) && is.array(resp) && resp.length) { this.newResults = true; if (!this._profileImage && this._isProfileWidget) { var name = resp[0].user.screen_name; this.setProfileImage(resp[0].user.profile_image_url); this.setTitle(resp[0].user.name); this.setCaption('@ ' + name + ''); } this.sinceId = resp[0].id_str; this._sortByMagic(resp); if (this.isRunning()) { this._play(); } } else { this.newResults = false; } this._setUrl(); if (this._isLive) { this.decay(); } }, /** * @private * gets the ball rolling with a new widget * and resets the interval job */ _play: function() { var that = this; if (this.runOnce) { this._hasNewSearchResults = true; } if (this._avatars) { this._preloadImages(this.results); } if (this._isRelativeTime && (this._behavior == 'all' || this._behavior == 'preloaded')) { each(this.byClass('twtr-timestamp', 'a', true), function(el) { el.innerHTML = timeAgo(el.getAttribute('time')); }); } if (!this._isLive || this._behavior == 'all' || this._behavior == 'preloaded') { each(this.results, function(needle) { if (needle.retweeted_status) { needle = needle.retweeted_status; } if (that._isProfileWidget) { needle.from_user = needle.user.screen_name; needle.profile_image_url = needle.user.profile_image_url; } if (that._isFavsWidget || that._isListWidget) { needle.from_user = needle.user.screen_name; needle.profile_image_url = needle.user.profile_image_url; } needle.id = needle.id_str; that._createTweet({ id: needle.id, user: needle.from_user, tweet: ify.clean(needle.text), avatar: needle.profile_image_url, created_at: needle.created_at, needle: needle }); var el = that.tweet.element; (that._behavior == 'all') ? that._appendSlideFade(el) : that._appendTweet(el); }); if (this._behavior != 'preloaded') { return this; } } return this; }, _normalizeTweet: function(needle) { var that = this; needle.views++; if (this._isProfileWidget) { needle.from_user = that.username; needle.profile_image_url = needle.user.profile_image_url; } if (this._isFavsWidget || this._isListWidget) { needle.from_user = needle.user.screen_name; needle.profile_image_url = needle.user.profile_image_url; } if (this._isFullScreen) { needle.profile_image_url = needle.profile_image_url.replace(/_normal\./, '_bigger.'); } needle.id = needle.id_str; this._createTweet({ id: needle.id, user: needle.from_user, tweet: ify.clean(needle.text), avatar: needle.profile_image_url, created_at: needle.created_at, needle: needle })._appendSlideFade(); }, _insertNewResultsNumber: function() { if (!this._hasNewSearchResults) { this._hasNewSearchResults = false; return; } if (this.runOnce && this._isSearchWidget) { var newResultsTotal = this.response.total > this.rpp ? this.response.total : this.response.results.length; var plural = newResultsTotal > 1 ? 's' : ''; var moreThan = (this.response.warning && this.response.warning.match(/adjusted since_id/)) ? 'more than' : ''; var el = document.createElement('div'); classes.add(el, 'twtr-new-results'); el.innerHTML = '
 
' + '
 
' + moreThan + ' ' + newResultsTotal + ' new tweet' + plural + ''; insertAfter(el, this.byClass('twtr-reference-tweet', 'div')); this._hasNewSearchResults = false; } }, /** * @private * helps transitions to be smooth */ _preloadImages: function(results) { if (this._isProfileWidget || this._isFavsWidget || this._isListWidget) { each(results, function(el) { var img = new Image(); img.src = matchUrlScheme(el.user.profile_image_url); }); } else { each(results, function(el) { (new Image()).src = matchUrlScheme(el.profile_image_url); }); } }, // FIXME: This seems like a bug in Occasionally. /** * @private * @return bool * tells the job whether to decay */ _decayDecider: function() { var r = false; if (!this.runOnce) { this.runOnce = true; r = true; } else if (this.newResults) { r = true; } return r; }, /** * @public * @return self * starts the cycle */ start: function() { var that = this; if (!this._rendered) { setTimeout(function() { that.start.call(that); }, 50); return this; } if (!this._isLive) { this._getResults(); } else { this.occasionalJob.start(); } this._isRunning = true; this._hasOfficiallyStarted = true; return this; }, /** * @public * @return self * stops the cycle */ stop: function() { this.occasionalJob.stop(); if (this.intervalJob) { this.intervalJob.stop(); } this._isRunning = false; return this; }, /** * @public * @return self * will pause the scrolling, but not stop polling for new results * useful for 'hover' interactions */ pause: function() { if (this.isRunning() && this.intervalJob) { this.intervalJob.stop(); classes.add(this.widgetEl, 'twtr-paused'); this._isRunning = false; } if (this._resumeTimer) { clearTimeout(this._resumeTimer); this._resumeTimer = null; } return this; }, /** * @public * @return self * it's like unpausing */ resume: function() { var that = this; if (!this.isRunning() && this._hasOfficiallyStarted && this.intervalJob) { this._resumeTimer = window.setTimeout(function() { that.intervalJob.start(); that._isRunning = true; classes.remove(that.widgetEl, 'twtr-paused'); }, 2000); } return this; }, /** * @public * @return bool * whether the widget is running */ isRunning: function() { return this._isRunning; }, /** * @public facade * @return self * convenience method to stop the cycle, then clear it out * widget can be reused if destroyed */ destroy: function() { this.stop(); this.clear(); this.runOnce = false; this._hasOfficiallyStarted = false; this._profileImage = false; this._isLive = true; this._tweetFilter = false; this._isScroll = false; this.newResults = false; this._isRunning = false; this.sinceId = 1; this.results = []; this.showedResults = []; this.occasionalJob.destroy(); if (this.jsonRequestRunning) { clearTimeout(this.jsonRequestTimer); } classes.remove(this.widgetEl, 'twtr-scroll'); this.removeEvents(); return this; } }; }(); })(); // Support Web Intents // http://dev.twitter.com/pages/intents var intentRegex = /twitter\.com(\:\d{2,4})?\/intent\/(\w+)/, shortIntents = { tweet: true, retweet:true, favorite:true }, windowOptions = 'scrollbars=yes,resizable=yes,toolbar=no,location=yes', winHeight = screen.height, winWidth = screen.width; function handleIntent(e) { e = e || window.event; var target = e.target || e.srcElement, m, width, height, left, top; while (target && target.nodeName.toLowerCase() !== 'a') { target = target.parentNode; } if (target && target.nodeName.toLowerCase() === 'a' && target.href) { m = target.href.match(intentRegex); if (m) { width = 550; height = (m[2] in shortIntents) ? 420 : 560; left = Math.round((winWidth / 2) - (width / 2)); top = 0; if (winHeight > height) { top = Math.round((winHeight / 2) - (height / 2)); } window.open(target.href, 'intent', windowOptions + ',width=' + width + ',height=' + height + ',left=' + left + ',top=' + top); e.returnValue = false; e.preventDefault && e.preventDefault(); } } } if (document.addEventListener) { document.addEventListener('click', handleIntent, false); } else if (document.attachEvent) { document.attachEvent('onclick', handleIntent); } // end Web Intents })(); // #end application closure