1 <!DOCTYPE html> 2 <html> 3 <head> 4 <style> 5 body { 6 font-family: helvetica, arial, sans-serif; 7 font-size: 12px; 8 overflow: hidden; 9 } 10 11 a { 12 color:#0000CC; 13 text-decoration: underline; 14 cursor: pointer; 15 } 16 17 .open_box { 18 display: block; 19 overflow: hidden; 20 margin-right: 4px; 21 margin-top: 2px; 22 height: 12px; 23 width: 12px; 24 float: left; 25 clear: left; 26 background-image: url(sprite_arrows.gif); 27 background-position: 0px -24px; 28 cursor: pointer; 29 } 30 31 .opened .open_box { 32 background-position:-12px -24px; 33 } 34 35 .item { 36 padding: 2px 0px; 37 } 38 39 .item_title { 40 display: block; 41 min-width: 300px; 42 padding-left: 15px; 43 cursor: pointer; 44 } 45 46 .item_desc { 47 min-width: 500px; 48 height: 0px; 49 display: block; 50 border: none; 51 padding: 0px; 52 margin: 0px; 53 -webkit-transition: height 0.2s ease-out; 54 } 55 56 #title { 57 display: block; 58 margin-left: auto; 59 } 60 61 .error { 62 white-space: nowrap; 63 color: red; 64 } 65 66 .more { 67 display: block; 68 text-align: right; 69 padding-top: 20px; 70 padding-right: 10px; 71 color: #88C; 72 } 73 74 </style> 75 <script id="iframe_script"> 76 function reportHeight() { 77 var msg = JSON.stringify({type:"size", size:document.body.offsetHeight}); 78 parent.postMessage(msg, "*"); 79 } 80 81 function frameLoaded() { 82 var links = document.getElementsByTagName("A"); 83 for (i = 0; i < links.length; i++) { 84 var class = links[i].className; 85 if (class != "item_title" && class != "open_box") { 86 links[i].addEventListener("click", showStory); 87 } 88 } 89 window.addEventListener("message", messageHandler); 90 } 91 92 function showStory(event) { 93 var href = event.currentTarget.href; 94 parent.postMessage(JSON.stringify({type:"show", url:href}), "*"); 95 event.preventDefault(); 96 } 97 98 function messageHandler(event) { 99 reportHeight(); 100 } 101 102 </script> 103 <script> 104 // Feed URL. 105 var feedUrl = 'http://news.google.com/?output=rss'; 106 107 // The XMLHttpRequest object that tries to load and parse the feed. 108 var req; 109 110 function main() { 111 req = new XMLHttpRequest(); 112 req.onload = handleResponse; 113 req.onerror = handleError; 114 req.open("GET", feedUrl, true); 115 req.send(null); 116 } 117 118 // Handles feed parsing errors. 119 function handleFeedParsingFailed(error) { 120 var feed = document.getElementById("feed"); 121 feed.className = "error"; 122 feed.innerText = "Error: " + error; 123 } 124 125 // Handles errors during the XMLHttpRequest. 126 function handleError() { 127 handleFeedParsingFailed('Failed to fetch RSS feed.'); 128 } 129 130 // Handles parsing the feed data we got back from XMLHttpRequest. 131 function handleResponse() { 132 var doc = req.responseXML; 133 if (!doc) { 134 handleFeedParsingFailed("Not a valid feed."); 135 return; 136 } 137 buildPreview(doc); 138 } 139 140 // The maximum number of feed items to show in the preview. 141 var maxFeedItems = 5; 142 143 // Where the more stories link should navigate to. 144 var moreStoriesUrl; 145 146 function buildPreview(doc) { 147 // Get the link to the feed source. 148 var link = doc.getElementsByTagName("link"); 149 var parentTag = link[0].parentNode.tagName; 150 if (parentTag != "item" && parentTag != "entry") { 151 moreStoriesUrl = link[0].textContent; 152 } 153 154 // Setup the title image. 155 var images = doc.getElementsByTagName("image"); 156 var titleImg; 157 if (images.length != 0) { 158 var urls = images[0].getElementsByTagName("url"); 159 if (urls.length != 0) { 160 titleImg = urls[0].textContent; 161 } 162 } 163 var img = document.getElementById("title"); 164 // Listen for mouse and key events 165 if (titleImg) { 166 img.src = titleImg; 167 if (moreStoriesUrl) { 168 document.getElementById("title_a").addEventListener("click", moreStories); 169 document.getElementById("title_a").addEventListener("keydown", 170 function(event) { 171 if (event.keyCode == 13) { 172 moreStories(event); 173 }}); 174 } 175 } else { 176 img.style.display = "none"; 177 } 178 179 // Construct the iframe's HTML. 180 var iframe_src = "<!doctype html><html><head><script>" + 181 document.getElementById("iframe_script").textContent + "<" + 182 "/script></head><body onload='frameLoaded();' " + 183 "style='padding:0px;margin:0px;'>"; 184 185 var feed = document.getElementById("feed"); 186 // Set ARIA role indicating the feed element has a tree structure 187 feed.setAttribute("role", "tree"); 188 189 var entries = doc.getElementsByTagName('entry'); 190 if (entries.length == 0) { 191 entries = doc.getElementsByTagName('item'); 192 } 193 var count = Math.min(entries.length, maxFeedItems); 194 for (var i = 0; i < count; i++) { 195 item = entries.item(i); 196 197 // Grab the title for the feed item. 198 var itemTitle = item.getElementsByTagName('title')[0]; 199 if (itemTitle) { 200 itemTitle = itemTitle.textContent; 201 } else { 202 itemTitle = "Unknown title"; 203 } 204 205 // Grab the description. 206 var itemDesc = item.getElementsByTagName('description')[0]; 207 if (!itemDesc) { 208 itemDesc = item.getElementsByTagName('summary')[0]; 209 if (!itemDesc) { 210 itemDesc = item.getElementsByTagName('content')[0]; 211 } 212 } 213 if (itemDesc) { 214 itemDesc = itemDesc.childNodes[0].nodeValue; 215 } else { 216 itemDesc = ''; 217 } 218 219 var item = document.createElement("div"); 220 item.className = "item"; 221 var box = document.createElement("div"); 222 box.className = "open_box"; 223 box.addEventListener("click", showDesc); 224 // Disable focusing on box image separately from rest of tree item 225 box.tabIndex = -1; 226 item.appendChild(box); 227 228 var title = document.createElement("a"); 229 title.className = "item_title"; 230 // Give title an ID for use with ARIA 231 title.id = "item" + i; 232 title.innerText = itemTitle; 233 title.addEventListener("click", showDesc); 234 title.addEventListener("keydown", keyHandlerShowDesc); 235 // Update aria-activedescendant property in response to focus change 236 // within the tree 237 title.addEventListener("focus", function(event) { 238 feed.setAttribute( 239 "aria-activedescendant", this.id); 240 }); 241 // Enable keyboard focus on the item title element 242 title.tabIndex = 0; 243 // Set ARIA role role indicating that the title element is a node in the 244 // tree structure 245 title.setAttribute("role", "treeitem"); 246 // Set the ARIA state indicating this tree item is currently collapsed. 247 title.setAttribute("aria-expanded", "false"); 248 // Set ARIA property indicating that all items are at the same hierarchical 249 // level (no nesting) 250 title.setAttribute("aria-level", "1"); 251 item.appendChild(title); 252 253 var desc = document.createElement("iframe"); 254 desc.scrolling = "no"; 255 desc.className = "item_desc"; 256 // Disable keyboard focus on elements in iFrames that have not been 257 // displayed yet 258 desc.tabIndex = -1; 259 260 item.appendChild(desc); 261 feed.appendChild(item); 262 263 // The story body is created as an iframe with a data: URL in order to 264 // isolate it from this page and protect against XSS. As a data URL, it 265 // has limited privileges and must communicate back using postMessage(). 266 desc.src="data:text/html," + iframe_src + itemDesc + "</body></html>"; 267 } 268 269 if (moreStoriesUrl) { 270 var more = document.createElement("a"); 271 more.className = "more"; 272 more.innerText = "More stories \u00BB"; 273 more.tabIndex = 0; 274 more.addEventListener("click", moreStories); 275 more.addEventListener("keydown", function(event) { 276 if (event.keyCode == 13) { 277 moreStories(event); 278 }}); 279 feed.appendChild(more); 280 } 281 } 282 283 // Show |url| in a new tab. 284 function showUrl(url) { 285 // Only allow http and https URLs. 286 if (url.indexOf("http:") != 0 && url.indexOf("https:") != 0) { 287 return; 288 } 289 chrome.tabs.create({url: url}); 290 } 291 292 function moreStories(event) { 293 showUrl(moreStoriesUrl); 294 } 295 296 function keyHandlerShowDesc(event) { 297 // Display content under heading when spacebar or right-arrow pressed 298 // Hide content when spacebar pressed again or left-arrow pressed 299 // Move to next heading when down-arrow pressed 300 // Move to previous heading when up-arrow pressed 301 if (event.keyCode == 32) { 302 showDesc(event); 303 } else if ((this.parentNode.className == "item opened") && 304 (event.keyCode == 37)) { 305 showDesc(event); 306 } else if ((this.parentNode.className == "item") && (event.keyCode == 39)) { 307 showDesc(event); 308 } else if (event.keyCode == 40) { 309 if (this.parentNode.nextSibling) { 310 this.parentNode.nextSibling.children[1].focus(); 311 } 312 } else if (event.keyCode == 38) { 313 if (this.parentNode.previousSibling) { 314 this.parentNode.previousSibling.children[1].focus(); 315 } 316 } 317 } 318 319 function showDesc(event) { 320 var item = event.currentTarget.parentNode; 321 var items = document.getElementsByClassName("item"); 322 for (var i = 0; i < items.length; i++) { 323 var iframe = items[i].getElementsByClassName("item_desc")[0]; 324 if (items[i] == item && items[i].className == "item") { 325 items[i].className = "item opened"; 326 iframe.contentWindow.postMessage("reportHeight", "*"); 327 // Set the ARIA state indicating the tree item is currently expanded. 328 items[i].getElementsByClassName("item_title")[0]. 329 setAttribute("aria-expanded", "true"); 330 iframe.tabIndex = 0; 331 } else { 332 items[i].className = "item"; 333 iframe.style.height = "0px"; 334 // Set the ARIA state indicating the tree item is currently collapsed. 335 items[i].getElementsByClassName("item_title")[0]. 336 setAttribute("aria-expanded", "false"); 337 iframe.tabIndex = -1; 338 } 339 } 340 } 341 342 function iframeMessageHandler(e) { 343 // Only listen to messages from one of our own iframes. 344 var iframes = document.getElementsByTagName("IFRAME"); 345 for (var i = 0; i < iframes.length; i++) { 346 if (iframes[i].contentWindow == e.source) { 347 var msg = JSON.parse(e.data); 348 if (msg) { 349 if (msg.type == "size") { 350 iframes[i].style.height = msg.size + "px"; 351 } else if (msg.type == "show") { 352 var url = msg.url; 353 if (url.indexOf("http://news.google.com") == 0) { 354 // If the URL is a redirect URL, strip of the destination and go to 355 // that directly. This is necessary because the Google news 356 // redirector blocks use of the redirects in this case. 357 var index = url.indexOf("&url="); 358 if (index >= 0) { 359 url = url.substring(index + 5); 360 index = url.indexOf("&"); 361 if (index >= 0) 362 url = url.substring(0, index); 363 } 364 } 365 showUrl(url); 366 } 367 } 368 return; 369 } 370 } 371 } 372 373 window.addEventListener("message", iframeMessageHandler); 374 </script> 375 </head> 376 <body onload="main();"> 377 <a id="title_a" tabIndex="0"><img id='title' alt="Google News logo"></a> 378 <div id="feed"></div> 379 </body> 380 </html> 381 382