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