1// Copyright (c) 2011 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// Feed 6var feedUrl = 'http://news.google.com/?output=rss'; 7 8// The XMLHttpRequest object that tries to load and parse the feed. 9var req; 10 11function main() { 12 req = new XMLHttpRequest(); 13 req.onload = handleResponse; 14 req.onerror = handleError; 15 req.open('GET', feedUrl, true); 16 req.send(null); 17} 18 19// Handles feed parsing errors. 20function handleFeedParsingFailed(error) { 21 var feed = document.getElementById('feed'); 22 feed.className = 'error'; 23 feed.innerText = 'Error: ' + error; 24} 25 26// Handles errors during the XMLHttpRequest. 27function handleError() { 28 handleFeedParsingFailed('Failed to fetch RSS feed.'); 29} 30 31// Handles parsing the feed data we got back from XMLHttpRequest. 32function handleResponse() { 33 var doc = req.responseXML; 34 if (!doc) { 35 handleFeedParsingFailed('Not a valid feed.'); 36 return; 37 } 38 buildPreview(doc); 39} 40 41// The maximum number of feed items to show in the preview. 42var maxFeedItems = 5; 43 44// Where the more stories link should navigate to. 45var moreStoriesUrl; 46 47function buildPreview(doc) { 48 // Get the link to the feed source. 49 var link = doc.getElementsByTagName('link'); 50 var parentTag = link[0].parentNode.tagName; 51 if (parentTag != 'item' && parentTag != 'entry') { 52 moreStoriesUrl = link[0].textContent; 53 } 54 55 // Setup the title image. 56 var images = doc.getElementsByTagName('image'); 57 var titleImg; 58 if (images.length != 0) { 59 var urls = images[0].getElementsByTagName('url'); 60 if (urls.length != 0) { 61 titleImg = urls[0].textContent; 62 } 63 } 64 var img = document.getElementById('title'); 65 // Listen for mouse and key events 66 if (titleImg) { 67 img.src = titleImg; 68 if (moreStoriesUrl) { 69 document.getElementById('title_a').addEventListener('click', 70 moreStories); 71 document.getElementById('title_a').addEventListener('keydown', 72 function(event) { 73 if (event.keyCode == 13) { 74 moreStories(event); 75 }}); 76 } 77 } else { 78 img.style.display = 'none'; 79 } 80 81 // Construct the iframe's HTML. 82 var iframe_src = '<!doctype html><html><head><script ' + 83 'src="chrome-extension://ldglnfnokeifbcaeppacaejckagballg/' + 84 'feed_iframe.js"><' + '/script><link href="chrome-extension://ldglnf' + 85 'nokeifbcaeppacaejckagballg/feed_iframe.css" rel="stylesheet" ' + 86 'type="text/css"></head><body>'; 87 88 var feed = document.getElementById('feed'); 89 // Set ARIA role indicating the feed element has a tree structure 90 feed.setAttribute('role', 'tree'); 91 92 var entries = doc.getElementsByTagName('entry'); 93 if (entries.length == 0) { 94 entries = doc.getElementsByTagName('item'); 95 } 96 var count = Math.min(entries.length, maxFeedItems); 97 for (var i = 0; i < count; i++) { 98 item = entries.item(i); 99 100 // Grab the title for the feed item. 101 var itemTitle = item.getElementsByTagName('title')[0]; 102 if (itemTitle) { 103 itemTitle = itemTitle.textContent; 104 } else { 105 itemTitle = 'Unknown title'; 106 } 107 108 // Grab the description. 109 var itemDesc = item.getElementsByTagName('description')[0]; 110 if (!itemDesc) { 111 itemDesc = item.getElementsByTagName('summary')[0]; 112 if (!itemDesc) { 113 itemDesc = item.getElementsByTagName('content')[0]; 114 } 115 } 116 if (itemDesc) { 117 itemDesc = itemDesc.childNodes[0].nodeValue; 118 } else { 119 itemDesc = ''; 120 } 121 122 var item = document.createElement('div'); 123 item.className = 'item'; 124 var box = document.createElement('div'); 125 box.className = 'open_box'; 126 box.addEventListener('click', showDesc); 127 // Disable focusing on box image separately from rest of tree item 128 box.tabIndex = -1; 129 item.appendChild(box); 130 131 var title = document.createElement('a'); 132 title.className = 'item_title'; 133 // Give title an ID for use with ARIA 134 title.id = 'item' + i; 135 title.innerText = itemTitle; 136 title.addEventListener('click', showDesc); 137 title.addEventListener('keydown', keyHandlerShowDesc); 138 // Update aria-activedescendant property in response to focus change 139 // within the tree 140 title.addEventListener('focus', function(event) { 141 feed.setAttribute( 142 'aria-activedescendant', this.id); 143 }); 144 // Enable keyboard focus on the item title element 145 title.tabIndex = 0; 146 // Set ARIA role role indicating that the title element is a node in the 147 // tree structure 148 title.setAttribute('role', 'treeitem'); 149 // Set the ARIA state indicating this tree item is currently collapsed. 150 title.setAttribute('aria-expanded', 'false'); 151 // Set ARIA property indicating that all items are at the same hierarchical 152 // level (no nesting) 153 title.setAttribute('aria-level', '1'); 154 item.appendChild(title); 155 156 var desc = document.createElement('iframe'); 157 desc.scrolling = 'no'; 158 desc.className = 'item_desc'; 159 // Disable keyboard focus on elements in iFrames that have not been 160 // displayed yet 161 desc.tabIndex = -1; 162 163 // The story body is created as an iframe with a data: URL in order to 164 // isolate it from this page and protect against XSS. As a data URL, it 165 // has limited privileges and must communicate back using postMessage(). 166 desc.src='data:text/html,' + iframe_src + itemDesc + '</body></html>'; 167 168 item.appendChild(desc); 169 feed.appendChild(item); 170 } 171 172 if (moreStoriesUrl) { 173 var more = document.createElement('a'); 174 more.className = 'more'; 175 more.innerText = 'More stories \u00BB'; 176 more.tabIndex = 0; 177 more.addEventListener('click', moreStories); 178 more.addEventListener('keydown', function(event) { 179 if (event.keyCode == 13) { 180 moreStories(event); 181 }}); 182 feed.appendChild(more); 183 } 184} 185 186// Show |url| in a new tab. 187function showUrl(url) { 188 // Only allow http and https URLs. 189 if (url.indexOf('http:') != 0 && url.indexOf('https:') != 0) { 190 return; 191 } 192 chrome.tabs.create({url: url}); 193} 194 195function moreStories(event) { 196 showUrl(moreStoriesUrl); 197} 198 199function keyHandlerShowDesc(event) { 200// Display content under heading when spacebar or right-arrow pressed 201// Hide content when spacebar pressed again or left-arrow pressed 202// Move to next heading when down-arrow pressed 203// Move to previous heading when up-arrow pressed 204 if (event.keyCode == 32) { 205 showDesc(event); 206 } else if ((this.parentNode.className == 'item opened') && 207 (event.keyCode == 37)) { 208 showDesc(event); 209 } else if ((this.parentNode.className == 'item') && (event.keyCode == 39)) { 210 showDesc(event); 211 } else if (event.keyCode == 40) { 212 if (this.parentNode.nextSibling) { 213 this.parentNode.nextSibling.children[1].focus(); 214 } 215 } else if (event.keyCode == 38) { 216 if (this.parentNode.previousSibling) { 217 this.parentNode.previousSibling.children[1].focus(); 218 } 219 } 220} 221 222function showDesc(event) { 223 var item = event.currentTarget.parentNode; 224 var items = document.getElementsByClassName('item'); 225 for (var i = 0; i < items.length; i++) { 226 var iframe = items[i].getElementsByClassName('item_desc')[0]; 227 if (items[i] == item && items[i].className == 'item') { 228 items[i].className = 'item opened'; 229 iframe.contentWindow.postMessage('reportHeight', '*'); 230 // Set the ARIA state indicating the tree item is currently expanded. 231 items[i].getElementsByClassName('item_title')[0]. 232 setAttribute('aria-expanded', 'true'); 233 iframe.tabIndex = 0; 234 } else { 235 items[i].className = 'item'; 236 iframe.style.height = '0px'; 237 // Set the ARIA state indicating the tree item is currently collapsed. 238 items[i].getElementsByClassName('item_title')[0]. 239 setAttribute('aria-expanded', 'false'); 240 iframe.tabIndex = -1; 241 } 242 } 243} 244 245function iframeMessageHandler(e) { 246 // Only listen to messages from one of our own iframes. 247 var iframes = document.getElementsByTagName('IFRAME'); 248 for (var i = 0; i < iframes.length; i++) { 249 if (iframes[i].contentWindow == e.source) { 250 var msg = JSON.parse(e.data); 251 if (msg) { 252 if (msg.type == 'size') { 253 iframes[i].style.height = msg.size + 'px'; 254 } else if (msg.type == 'show') { 255 var url = msg.url; 256 if (url.indexOf('http://news.google.com') == 0) { 257 // If the URL is a redirect URL, strip of the destination and go to 258 // that directly. This is necessary because the Google news 259 // redirector blocks use of the redirects in this case. 260 var index = url.indexOf('&url='); 261 if (index >= 0) { 262 url = url.substring(index + 5); 263 index = url.indexOf('&'); 264 if (index >= 0) 265 url = url.substring(0, index); 266 } 267 } 268 showUrl(url); 269 } 270 } 271 return; 272 } 273 } 274} 275 276window.addEventListener('message', iframeMessageHandler); 277document.addEventListener('DOMContentLoaded', main); 278