| OLD | NEW |
| 1 <!DOCTYPE HTML> | 1 <!DOCTYPE HTML> |
| 2 <html i18n-values="dir:textdirection;"> | 2 <html i18n-values="dir:textdirection;"> |
| 3 <head> | 3 <head> |
| 4 <meta charset="utf-8"> | 4 <meta charset="utf-8"> |
| 5 <include src="content_security_policy.html"> |
| 5 <title i18n-content="title"></title> | 6 <title i18n-content="title"></title> |
| 6 <link rel="icon" href="../../app/theme/history_favicon.png"> | 7 <link rel="icon" href="../../app/theme/history_favicon.png"> |
| 7 <script src="shared/js/local_strings.js"></script> | 8 <script src="chrome://resources/js/local_strings.js"></script> |
| 8 <script> | 9 <script src="chrome://history2/history2.js"></script> |
| 9 /////////////////////////////////////////////////////////////////////////////// | 10 <script src="chrome://history2/strings.js"></script> |
| 10 // Globals: | |
| 11 var RESULTS_PER_PAGE = 150; | |
| 12 var MAX_SEARCH_DEPTH_MONTHS = 18; | |
| 13 | |
| 14 // Amount of time between pageviews that we consider a 'break' in browsing, | |
| 15 // measured in milliseconds. | |
| 16 var BROWSING_GAP_TIME = 15 * 60 * 1000; | |
| 17 | |
| 18 function $(o) {return document.getElementById(o);} | |
| 19 | |
| 20 function createElementWithClassName(type, className) { | |
| 21 var elm = document.createElement(type); | |
| 22 elm.className = className; | |
| 23 return elm; | |
| 24 } | |
| 25 | |
| 26 // Escapes a URI as appropriate for CSS. | |
| 27 function encodeURIForCSS(uri) { | |
| 28 // CSS uris need to have '(' and ')' escaped. | |
| 29 return uri.replace(/\(/g, "\\(").replace(/\)/g, "\\)"); | |
| 30 } | |
| 31 | |
| 32 // TODO(glen): Get rid of these global references, replace with a controller | |
| 33 // or just make the classes own more of the page. | |
| 34 var historyModel; | |
| 35 var historyView; | |
| 36 var localStrings; | |
| 37 var pageState; | |
| 38 var deleteQueue = []; | |
| 39 var deleteInFlight = false; | |
| 40 var selectionAnchor = -1; | |
| 41 var idToCheckbox = []; | |
| 42 | |
| 43 | |
| 44 /////////////////////////////////////////////////////////////////////////////// | |
| 45 // Page: | |
| 46 /** | |
| 47 * Class to hold all the information about an entry in our model. | |
| 48 * @param {Object} result An object containing the page's data. | |
| 49 * @param {boolean} continued Whether this page is on the same day as the | |
| 50 * page before it | |
| 51 */ | |
| 52 function Page(result, continued, model, id) { | |
| 53 this.model_ = model; | |
| 54 this.title_ = result.title; | |
| 55 this.url_ = result.url; | |
| 56 this.domain_ = this.getDomainFromURL_(this.url_); | |
| 57 this.starred_ = result.starred; | |
| 58 this.snippet_ = result.snippet || ""; | |
| 59 this.id_ = id; | |
| 60 | |
| 61 this.changed = false; | |
| 62 | |
| 63 this.isRendered = false; | |
| 64 | |
| 65 // All the date information is public so that owners can compare properties of | |
| 66 // two items easily. | |
| 67 | |
| 68 // We get the time in seconds, but we want it in milliseconds. | |
| 69 this.time = new Date(result.time * 1000); | |
| 70 | |
| 71 // See comment in BrowsingHistoryHandler::QueryComplete - we won't always | |
| 72 // get all of these. | |
| 73 this.dateRelativeDay = result.dateRelativeDay || ""; | |
| 74 this.dateTimeOfDay = result.dateTimeOfDay || ""; | |
| 75 this.dateShort = result.dateShort || ""; | |
| 76 | |
| 77 // Whether this is the continuation of a previous day. | |
| 78 this.continued = continued; | |
| 79 } | |
| 80 | |
| 81 // Page, Public: -------------------------------------------------------------- | |
| 82 /** | |
| 83 * Returns a dom structure for a browse page result or a search page result. | |
| 84 * @param {boolean} Flag to indicate if result is a search result. | |
| 85 * @return {Element} The dom structure. | |
| 86 */ | |
| 87 Page.prototype.getResultDOM = function(searchResultFlag) { | |
| 88 var node = createElementWithClassName('li', 'entry'); | |
| 89 var time = createElementWithClassName('div', 'time'); | |
| 90 var domain = createElementWithClassName('span', 'domain'); | |
| 91 domain.style.backgroundImage = | |
| 92 'url(chrome://favicon/' + encodeURIForCSS(this.url_) + ')'; | |
| 93 domain.textContent = this.domain_; | |
| 94 node.appendChild(time); | |
| 95 node.appendChild(domain); | |
| 96 node.appendChild(this.getTitleDOM_()); | |
| 97 if (searchResultFlag) { | |
| 98 time.textContent = this.dateShort; | |
| 99 var snippet = createElementWithClassName('div', 'snippet'); | |
| 100 this.addHighlightedText_(snippet, | |
| 101 this.snippet_, | |
| 102 this.model_.getSearchText()); | |
| 103 node.appendChild(snippet); | |
| 104 } else { | |
| 105 if (this.model_.getEditMode()) { | |
| 106 var checkbox = document.createElement('input'); | |
| 107 checkbox.type = 'checkbox'; | |
| 108 checkbox.name = this.id_; | |
| 109 checkbox.time = this.time.toString(); | |
| 110 checkbox.addEventListener("click", checkboxClicked); | |
| 111 idToCheckbox[this.id_] = checkbox; | |
| 112 time.appendChild(checkbox); | |
| 113 } | |
| 114 time.appendChild(document.createTextNode(this.dateTimeOfDay)); | |
| 115 } | |
| 116 return node; | |
| 117 }; | |
| 118 | |
| 119 // Page, private: ------------------------------------------------------------- | |
| 120 /** | |
| 121 * Extracts and returns the domain (and subdomains) from a URL. | |
| 122 * @param {string} The url | |
| 123 * @return (string) The domain. An empty string is returned if no domain can | |
| 124 * be found. | |
| 125 */ | |
| 126 Page.prototype.getDomainFromURL_ = function(url) { | |
| 127 var domain = url.replace(/^.+:\/\//, '').match(/[^/]+/); | |
| 128 return domain ? domain[0] : ''; | |
| 129 }; | |
| 130 | |
| 131 /** | |
| 132 * Truncates a string to a maximum lenth (including ... if truncated) | |
| 133 * @param {string} The string to be truncated | |
| 134 * @param {number} The length to truncate the string to | |
| 135 * @return (string) The truncated string | |
| 136 */ | |
| 137 Page.prototype.truncateString_ = function(str, maxLength) { | |
| 138 if (str.length > maxLength) { | |
| 139 return str.substr(0, maxLength - 3) + '...'; | |
| 140 } else { | |
| 141 return str; | |
| 142 } | |
| 143 }; | |
| 144 | |
| 145 /** | |
| 146 * Add child text nodes to a node such that occurrences of the spcified text is | |
| 147 * highligted. | |
| 148 * @param {Node} node The node under which new text nodes will be made as | |
| 149 * children. | |
| 150 * @param {string} content Text to be added beneath |node| as one or more | |
| 151 * text nodes. | |
| 152 * @param {string} highlightText Occurences of this text inside |content| will | |
| 153 * be highlighted. | |
| 154 */ | |
| 155 Page.prototype.addHighlightedText_ = function(node, content, highlightText) { | |
| 156 var i = 0; | |
| 157 if (highlightText) { | |
| 158 var re = new RegExp(Page.pregQuote_(highlightText), 'gim'); | |
| 159 var match; | |
| 160 while (match = re.exec(content)) { | |
| 161 if (match.index > i) | |
| 162 node.appendChild(document.createTextNode(content.slice(i, | |
| 163 match.index))); | |
| 164 i = re.lastIndex; | |
| 165 // Mark the highlighted text in bold. | |
| 166 var b = document.createElement('b'); | |
| 167 b.textContent = content.substring(match.index, i); | |
| 168 node.appendChild(b); | |
| 169 } | |
| 170 } | |
| 171 if (i < content.length) | |
| 172 node.appendChild(document.createTextNode(content.slice(i))); | |
| 173 }; | |
| 174 | |
| 175 /** | |
| 176 * @return {DOMObject} DOM representation for the title block. | |
| 177 */ | |
| 178 Page.prototype.getTitleDOM_ = function() { | |
| 179 var node = document.createElement('span'); | |
| 180 node.className = 'title'; | |
| 181 var link = document.createElement('a'); | |
| 182 link.href = this.url_; | |
| 183 link.id = "id-" + this.id_; | |
| 184 | |
| 185 var content = this.truncateString_(this.title_, 80); | |
| 186 | |
| 187 // If we have truncated the title, add a tooltip. | |
| 188 if (content.length != this.title_.length) { | |
| 189 link.title = this.title_; | |
| 190 } | |
| 191 this.addHighlightedText_(link, content, this.model_.getSearchText()); | |
| 192 node.appendChild(link); | |
| 193 | |
| 194 if (this.starred_) { | |
| 195 node.className += ' starred'; | |
| 196 node.appendChild(createElementWithClassName('div', 'starred')); | |
| 197 } | |
| 198 | |
| 199 return node; | |
| 200 }; | |
| 201 | |
| 202 // Page, private, static: ----------------------------------------------------- | |
| 203 | |
| 204 /** | |
| 205 * Quote a string so it can be used in a regular expression. | |
| 206 * @param {string} str The source string | |
| 207 * @return {string} The escaped string | |
| 208 */ | |
| 209 Page.pregQuote_ = function(str) { | |
| 210 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); | |
| 211 }; | |
| 212 | |
| 213 /////////////////////////////////////////////////////////////////////////////// | |
| 214 // HistoryModel: | |
| 215 /** | |
| 216 * Global container for history data. Future optimizations might include | |
| 217 * allowing the creation of a HistoryModel for each search string, allowing | |
| 218 * quick flips back and forth between results. | |
| 219 * | |
| 220 * The history model is based around pages, and only fetching the data to | |
| 221 * fill the currently requested page. This is somewhat dependent on the view, | |
| 222 * and so future work may wish to change history model to operate on | |
| 223 * timeframe (day or week) based containers. | |
| 224 */ | |
| 225 function HistoryModel() { | |
| 226 this.clearModel_(); | |
| 227 this.setEditMode(false); | |
| 228 this.view_; | |
| 229 } | |
| 230 | |
| 231 // HistoryModel, Public: ------------------------------------------------------ | |
| 232 /** | |
| 233 * Sets our current view that is called when the history model changes. | |
| 234 * @param {HistoryView} view The view to set our current view to. | |
| 235 */ | |
| 236 HistoryModel.prototype.setView = function(view) { | |
| 237 this.view_ = view; | |
| 238 }; | |
| 239 | |
| 240 /** | |
| 241 * Start a new search - this will clear out our model. | |
| 242 * @param {String} searchText The text to search for | |
| 243 * @param {Number} opt_page The page to view - this is mostly used when setting | |
| 244 * up an initial view, use #requestPage otherwise. | |
| 245 */ | |
| 246 HistoryModel.prototype.setSearchText = function(searchText, opt_page) { | |
| 247 this.clearModel_(); | |
| 248 this.searchText_ = searchText; | |
| 249 this.requestedPage_ = opt_page ? opt_page : 0; | |
| 250 this.getSearchResults_(); | |
| 251 }; | |
| 252 | |
| 253 /** | |
| 254 * Reload our model with the current parameters. | |
| 255 */ | |
| 256 HistoryModel.prototype.reload = function() { | |
| 257 var search = this.searchText_; | |
| 258 var page = this.requestedPage_; | |
| 259 this.clearModel_(); | |
| 260 this.searchText_ = search; | |
| 261 this.requestedPage_ = page; | |
| 262 this.getSearchResults_(); | |
| 263 }; | |
| 264 | |
| 265 /** | |
| 266 * @return {String} The current search text. | |
| 267 */ | |
| 268 HistoryModel.prototype.getSearchText = function() { | |
| 269 return this.searchText_; | |
| 270 }; | |
| 271 | |
| 272 /** | |
| 273 * Tell the model that the view will want to see the current page. When | |
| 274 * the data becomes available, the model will call the view back. | |
| 275 * @page {Number} page The page we want to view. | |
| 276 */ | |
| 277 HistoryModel.prototype.requestPage = function(page) { | |
| 278 this.requestedPage_ = page; | |
| 279 this.changed = true; | |
| 280 this.updateSearch_(false); | |
| 281 }; | |
| 282 | |
| 283 /** | |
| 284 * Receiver for history query. | |
| 285 * @param {String} term The search term that the results are for. | |
| 286 * @param {Array} results A list of results | |
| 287 */ | |
| 288 HistoryModel.prototype.addResults = function(info, results) { | |
| 289 this.inFlight_ = false; | |
| 290 if (info.term != this.searchText_) { | |
| 291 // If our results aren't for our current search term, they're rubbish. | |
| 292 return; | |
| 293 } | |
| 294 | |
| 295 // Currently we assume we're getting things in date order. This needs to | |
| 296 // be updated if that ever changes. | |
| 297 if (results) { | |
| 298 var lastURL, lastDay; | |
| 299 var oldLength = this.pages_.length; | |
| 300 if (oldLength) { | |
| 301 var oldPage = this.pages_[oldLength - 1]; | |
| 302 lastURL = oldPage.url; | |
| 303 lastDay = oldPage.dateRelativeDay; | |
| 304 } | |
| 305 | |
| 306 for (var i = 0, thisResult; thisResult = results[i]; i++) { | |
| 307 var thisURL = thisResult.url; | |
| 308 var thisDay = thisResult.dateRelativeDay; | |
| 309 | |
| 310 // Remove adjacent duplicates. | |
| 311 if (!lastURL || lastURL != thisURL) { | |
| 312 // Figure out if this page is in the same day as the previous page, | |
| 313 // this is used to determine how day headers should be drawn. | |
| 314 this.pages_.push(new Page(thisResult, thisDay == lastDay, this, | |
| 315 this.last_id_++)); | |
| 316 lastDay = thisDay; | |
| 317 lastURL = thisURL; | |
| 318 } | |
| 319 } | |
| 320 if (results.length) | |
| 321 this.changed = true; | |
| 322 } | |
| 323 | |
| 324 this.updateSearch_(info.finished); | |
| 325 }; | |
| 326 | |
| 327 /** | |
| 328 * @return {Number} The number of pages in the model. | |
| 329 */ | |
| 330 HistoryModel.prototype.getSize = function() { | |
| 331 return this.pages_.length; | |
| 332 }; | |
| 333 | |
| 334 /** | |
| 335 * @return {boolean} Whether our history query has covered all of | |
| 336 * the user's history | |
| 337 */ | |
| 338 HistoryModel.prototype.isComplete = function() { | |
| 339 return this.complete_; | |
| 340 }; | |
| 341 | |
| 342 /** | |
| 343 * Get a list of pages between specified index positions. | |
| 344 * @param {Number} start The start index | |
| 345 * @param {Number} end The end index | |
| 346 * @return {Array} A list of pages | |
| 347 */ | |
| 348 HistoryModel.prototype.getNumberedRange = function(start, end) { | |
| 349 if (start >= this.getSize()) | |
| 350 return []; | |
| 351 | |
| 352 var end = end > this.getSize() ? this.getSize() : end; | |
| 353 return this.pages_.slice(start, end); | |
| 354 }; | |
| 355 | |
| 356 /** | |
| 357 * @return {boolean} Whether we are in edit mode where history items can be | |
| 358 * deleted | |
| 359 */ | |
| 360 HistoryModel.prototype.getEditMode = function() { | |
| 361 return this.editMode_; | |
| 362 }; | |
| 363 | |
| 364 /** | |
| 365 * @param {boolean} edit_mode Control whether we are in edit mode. | |
| 366 */ | |
| 367 HistoryModel.prototype.setEditMode = function(edit_mode) { | |
| 368 this.editMode_ = edit_mode; | |
| 369 }; | |
| 370 | |
| 371 // HistoryModel, Private: ----------------------------------------------------- | |
| 372 HistoryModel.prototype.clearModel_ = function() { | |
| 373 this.inFlight_ = false; // Whether a query is inflight. | |
| 374 this.searchText_ = ''; | |
| 375 this.searchDepth_ = 0; | |
| 376 this.pages_ = []; // Date-sorted list of pages. | |
| 377 this.last_id_ = 0; | |
| 378 selectionAnchor = -1; | |
| 379 idToCheckbox = []; | |
| 380 | |
| 381 // The page that the view wants to see - we only fetch slightly past this | |
| 382 // point. If the view requests a page that we don't have data for, we try | |
| 383 // to fetch it and call back when we're done. | |
| 384 this.requestedPage_ = 0; | |
| 385 | |
| 386 this.complete_ = false; | |
| 387 | |
| 388 if (this.view_) { | |
| 389 this.view_.clear_(); | |
| 390 } | |
| 391 }; | |
| 392 | |
| 393 /** | |
| 394 * Figure out if we need to do more searches to fill the currently requested | |
| 395 * page. If we think we can fill the page, call the view and let it know | |
| 396 * we're ready to show something. | |
| 397 */ | |
| 398 HistoryModel.prototype.updateSearch_ = function(finished) { | |
| 399 if ((this.searchText_ && this.searchDepth_ >= MAX_SEARCH_DEPTH_MONTHS) || | |
| 400 finished) { | |
| 401 // We have maxed out. There will be no more data. | |
| 402 this.complete_ = true; | |
| 403 this.view_.onModelReady(); | |
| 404 this.changed = false; | |
| 405 } else { | |
| 406 // If we can't fill the requested page, ask for more data unless a request | |
| 407 // is still in-flight. | |
| 408 if (!this.canFillPage_(this.requestedPage_) && !this.inFlight_) { | |
| 409 this.getSearchResults_(this.searchDepth_ + 1); | |
| 410 } | |
| 411 | |
| 412 // If we have any data for the requested page, show it. | |
| 413 if (this.changed && this.haveDataForPage_(this.requestedPage_)) { | |
| 414 this.view_.onModelReady(); | |
| 415 this.changed = false; | |
| 416 } | |
| 417 } | |
| 418 }; | |
| 419 | |
| 420 /** | |
| 421 * Get search results for a selected depth. Our history system is optimized | |
| 422 * for queries that don't cross month boundaries, but an entire month's | |
| 423 * worth of data is huge. When we're in browse mode (searchText is empty) | |
| 424 * we request the data a day at a time. When we're searching, a month is | |
| 425 * used. | |
| 426 * | |
| 427 * TODO: Fix this for when the user's clock goes across month boundaries. | |
| 428 * @param {number} opt_day How many days back to do the search. | |
| 429 */ | |
| 430 HistoryModel.prototype.getSearchResults_ = function(depth) { | |
| 431 this.searchDepth_ = depth || 0; | |
| 432 | |
| 433 if (this.searchText_ == "") { | |
| 434 chrome.send('getHistory', | |
| 435 [String(this.searchDepth_)]); | |
| 436 } else { | |
| 437 chrome.send('searchHistory', | |
| 438 [this.searchText_, String(this.searchDepth_)]); | |
| 439 } | |
| 440 | |
| 441 this.inFlight_ = true; | |
| 442 }; | |
| 443 | |
| 444 /** | |
| 445 * Check to see if we have data for a given page. | |
| 446 * @param {number} page The page number | |
| 447 * @return {boolean} Whether we have any data for the given page. | |
| 448 */ | |
| 449 HistoryModel.prototype.haveDataForPage_ = function(page) { | |
| 450 return (page * RESULTS_PER_PAGE < this.getSize()); | |
| 451 }; | |
| 452 | |
| 453 /** | |
| 454 * Check to see if we have data to fill a page. | |
| 455 * @param {number} page The page number. | |
| 456 * @return {boolean} Whether we have data to fill the page. | |
| 457 */ | |
| 458 HistoryModel.prototype.canFillPage_ = function(page) { | |
| 459 return ((page + 1) * RESULTS_PER_PAGE <= this.getSize()); | |
| 460 }; | |
| 461 | |
| 462 /////////////////////////////////////////////////////////////////////////////// | |
| 463 // HistoryView: | |
| 464 /** | |
| 465 * Functions and state for populating the page with HTML. This should one-day | |
| 466 * contain the view and use event handlers, rather than pushing HTML out and | |
| 467 * getting called externally. | |
| 468 * @param {HistoryModel} model The model backing this view. | |
| 469 */ | |
| 470 function HistoryView(model) { | |
| 471 this.summaryTd_ = $('results-summary'); | |
| 472 this.summaryTd_.textContent = localStrings.getString('loading'); | |
| 473 this.editButtonTd_ = $('edit-button'); | |
| 474 this.editingControlsDiv_ = $('editing-controls'); | |
| 475 this.resultDiv_ = $('results-display'); | |
| 476 this.pageDiv_ = $('results-pagination'); | |
| 477 this.model_ = model | |
| 478 this.pageIndex_ = 0; | |
| 479 this.lastDisplayed_ = []; | |
| 480 | |
| 481 this.model_.setView(this); | |
| 482 | |
| 483 this.currentPages_ = []; | |
| 484 | |
| 485 var self = this; | |
| 486 window.onresize = function() { | |
| 487 self.updateEntryAnchorWidth_(); | |
| 488 }; | |
| 489 self.updateEditControls_(); | |
| 490 | |
| 491 this.boundUpdateRemoveButton_ = function(e) { | |
| 492 return self.updateRemoveButton_(e); | |
| 493 }; | |
| 494 } | |
| 495 | |
| 496 // HistoryView, public: ------------------------------------------------------- | |
| 497 /** | |
| 498 * Do a search and optionally view a certain page. | |
| 499 * @param {string} term The string to search for. | |
| 500 * @param {number} opt_page The page we wish to view, only use this for | |
| 501 * setting up initial views, as this triggers a search. | |
| 502 */ | |
| 503 HistoryView.prototype.setSearch = function(term, opt_page) { | |
| 504 this.pageIndex_ = parseInt(opt_page || 0, 10); | |
| 505 window.scrollTo(0, 0); | |
| 506 this.model_.setSearchText(term, this.pageIndex_); | |
| 507 if (term) { | |
| 508 this.setEditMode(false); | |
| 509 } | |
| 510 this.updateEditControls_(); | |
| 511 pageState.setUIState(this.model_.getEditMode(), term, this.pageIndex_); | |
| 512 }; | |
| 513 | |
| 514 /** | |
| 515 * Controls edit mode where history can be deleted. | |
| 516 * @param {boolean} edit_mode Whether to enable edit mode. | |
| 517 */ | |
| 518 HistoryView.prototype.setEditMode = function(edit_mode) { | |
| 519 this.model_.setEditMode(edit_mode); | |
| 520 pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(), | |
| 521 this.pageIndex_); | |
| 522 }; | |
| 523 | |
| 524 /** | |
| 525 * Toggles the edit mode and triggers UI update. | |
| 526 */ | |
| 527 HistoryView.prototype.toggleEditMode = function() { | |
| 528 var editMode = !this.model_.getEditMode(); | |
| 529 this.setEditMode(editMode); | |
| 530 this.updateEditControls_(); | |
| 531 }; | |
| 532 | |
| 533 /** | |
| 534 * @return {boolean} Whether we are in edit mode where history items can be | |
| 535 * deleted | |
| 536 */ | |
| 537 HistoryView.prototype.getEditMode = function() { | |
| 538 return this.model_.getEditMode(); | |
| 539 }; | |
| 540 | |
| 541 /** | |
| 542 * Reload the current view. | |
| 543 */ | |
| 544 HistoryView.prototype.reload = function() { | |
| 545 this.model_.reload(); | |
| 546 }; | |
| 547 | |
| 548 /** | |
| 549 * Switch to a specified page. | |
| 550 * @param {number} page The page we wish to view. | |
| 551 */ | |
| 552 HistoryView.prototype.setPage = function(page) { | |
| 553 this.clear_(); | |
| 554 this.pageIndex_ = parseInt(page, 10); | |
| 555 window.scrollTo(0, 0); | |
| 556 this.model_.requestPage(page); | |
| 557 pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(), | |
| 558 this.pageIndex_); | |
| 559 }; | |
| 560 | |
| 561 /** | |
| 562 * @return {number} The page number being viewed. | |
| 563 */ | |
| 564 HistoryView.prototype.getPage = function() { | |
| 565 return this.pageIndex_; | |
| 566 }; | |
| 567 | |
| 568 /** | |
| 569 * Callback for the history model to let it know that it has data ready for us | |
| 570 * to view. | |
| 571 */ | |
| 572 HistoryView.prototype.onModelReady = function() { | |
| 573 this.displayResults_(); | |
| 574 }; | |
| 575 | |
| 576 // HistoryView, private: ------------------------------------------------------ | |
| 577 /** | |
| 578 * Clear the results in the view. Since we add results piecemeal, we need | |
| 579 * to clear them out when we switch to a new page or reload. | |
| 580 */ | |
| 581 HistoryView.prototype.clear_ = function() { | |
| 582 this.resultDiv_.textContent = ''; | |
| 583 | |
| 584 var pages = this.currentPages_; | |
| 585 for (var i = 0; i < pages.length; i++) { | |
| 586 pages[i].isRendered = false; | |
| 587 } | |
| 588 this.currentPages_ = []; | |
| 589 }; | |
| 590 | |
| 591 HistoryView.prototype.setPageRendered_ = function(page) { | |
| 592 page.isRendered = true; | |
| 593 this.currentPages_.push(page); | |
| 594 }; | |
| 595 | |
| 596 /** | |
| 597 * Update the page with results. | |
| 598 */ | |
| 599 HistoryView.prototype.displayResults_ = function() { | |
| 600 var results = this.model_.getNumberedRange( | |
| 601 this.pageIndex_ * RESULTS_PER_PAGE, | |
| 602 this.pageIndex_ * RESULTS_PER_PAGE + RESULTS_PER_PAGE); | |
| 603 | |
| 604 if (this.model_.getSearchText()) { | |
| 605 var searchResults = createElementWithClassName('ol', 'search-results'); | |
| 606 for (var i = 0, page; page = results[i]; i++) { | |
| 607 if (!page.isRendered) { | |
| 608 searchResults.appendChild(page.getResultDOM(true)); | |
| 609 this.setPageRendered_(page); | |
| 610 } | |
| 611 } | |
| 612 this.resultDiv_.appendChild(searchResults); | |
| 613 } else { | |
| 614 var resultsFragment = document.createDocumentFragment(); | |
| 615 var lastTime = Math.infinity; | |
| 616 var dayResults; | |
| 617 for (var i = 0, page; page = results[i]; i++) { | |
| 618 if (page.isRendered) { | |
| 619 continue; | |
| 620 } | |
| 621 // Break across day boundaries and insert gaps for browsing pauses. | |
| 622 // Create a dayResults element to contain results for each day | |
| 623 var thisTime = page.time.getTime(); | |
| 624 | |
| 625 if ((i == 0 && page.continued) || !page.continued) { | |
| 626 var day = createElementWithClassName('h2', 'day'); | |
| 627 day.appendChild(document.createTextNode(page.dateRelativeDay)); | |
| 628 if (i == 0 && page.continued) { | |
| 629 day.appendChild(document.createTextNode(' ' + | |
| 630 localStrings.getString('cont'))); | |
| 631 } | |
| 632 | |
| 633 // If there is an existing dayResults element, append it. | |
| 634 if (dayResults) { | |
| 635 resultsFragment.appendChild(dayResults); | |
| 636 } | |
| 637 resultsFragment.appendChild(day); | |
| 638 dayResults = createElementWithClassName('ol', 'day-results'); | |
| 639 } else if (lastTime - thisTime > BROWSING_GAP_TIME) { | |
| 640 if (dayResults) { | |
| 641 dayResults.appendChild(createElementWithClassName('li', 'gap')); | |
| 642 } | |
| 643 } | |
| 644 lastTime = thisTime; | |
| 645 // Add entry. | |
| 646 if (dayResults) { | |
| 647 dayResults.appendChild(page.getResultDOM(false)); | |
| 648 this.setPageRendered_(page); | |
| 649 } | |
| 650 } | |
| 651 // Add final dayResults element. | |
| 652 if (dayResults) { | |
| 653 resultsFragment.appendChild(dayResults); | |
| 654 } | |
| 655 this.resultDiv_.appendChild(resultsFragment); | |
| 656 } | |
| 657 | |
| 658 this.displaySummaryBar_(); | |
| 659 this.displayNavBar_(); | |
| 660 this.updateEntryAnchorWidth_(); | |
| 661 }; | |
| 662 | |
| 663 /** | |
| 664 * Update the summary bar with descriptive text. | |
| 665 */ | |
| 666 HistoryView.prototype.displaySummaryBar_ = function() { | |
| 667 var searchText = this.model_.getSearchText(); | |
| 668 if (searchText != '') { | |
| 669 this.summaryTd_.textContent = localStrings.getStringF('searchresultsfor', | |
| 670 searchText); | |
| 671 } else { | |
| 672 this.summaryTd_.textContent = localStrings.getString('history'); | |
| 673 } | |
| 674 }; | |
| 675 | |
| 676 /** | |
| 677 * Update the widgets related to edit mode. | |
| 678 */ | |
| 679 HistoryView.prototype.updateEditControls_ = function() { | |
| 680 // Display a button (looking like a link) to enable/disable edit mode. | |
| 681 var oldButton = this.editButtonTd_.firstChild; | |
| 682 if (this.model_.getSearchText()) { | |
| 683 this.editButtonTd_.replaceChild(document.createElement('p'), oldButton); | |
| 684 this.editingControlsDiv_.textContent = ''; | |
| 685 return; | |
| 686 } | |
| 687 | |
| 688 var editMode = this.model_.getEditMode(); | |
| 689 var button = createElementWithClassName('button', 'edit-button'); | |
| 690 button.onclick = toggleEditMode; | |
| 691 button.textContent = localStrings.getString(editMode ? | |
| 692 'doneediting' : 'edithistory'); | |
| 693 this.editButtonTd_.replaceChild(button, oldButton); | |
| 694 | |
| 695 this.editingControlsDiv_.textContent = ''; | |
| 696 | |
| 697 if (editMode) { | |
| 698 // Button to delete the selected items. | |
| 699 button = document.createElement('button'); | |
| 700 button.onclick = removeItems; | |
| 701 button.textContent = localStrings.getString('removeselected'); | |
| 702 button.disabled = true; | |
| 703 this.editingControlsDiv_.appendChild(button); | |
| 704 this.removeButton_ = button; | |
| 705 | |
| 706 // Button that opens up the clear browsing data dialog. | |
| 707 button = document.createElement('button'); | |
| 708 button.onclick = openClearBrowsingData; | |
| 709 button.textContent = localStrings.getString('clearallhistory'); | |
| 710 this.editingControlsDiv_.appendChild(button); | |
| 711 | |
| 712 // Listen for clicks in the page to sync the disabled state. | |
| 713 document.addEventListener('click', this.boundUpdateRemoveButton_); | |
| 714 } else { | |
| 715 this.removeButton_ = null; | |
| 716 document.removeEventListener('click', this.boundUpdateRemoveButton_); | |
| 717 } | |
| 718 }; | |
| 719 | |
| 720 /** | |
| 721 * Updates the disabled state of the remove button when in editing mode. | |
| 722 * @param {!Event} e The click event object. | |
| 723 * @private | |
| 724 */ | |
| 725 HistoryView.prototype.updateRemoveButton_ = function(e) { | |
| 726 if (e.target.tagName != 'INPUT') | |
| 727 return; | |
| 728 | |
| 729 var anyChecked = document.querySelector('.entry input:checked') != null; | |
| 730 if (this.removeButton_) | |
| 731 this.removeButton_.disabled = !anyChecked; | |
| 732 }; | |
| 733 | |
| 734 /** | |
| 735 * Update the pagination tools. | |
| 736 */ | |
| 737 HistoryView.prototype.displayNavBar_ = function() { | |
| 738 this.pageDiv_.textContent = ''; | |
| 739 | |
| 740 if (this.pageIndex_ > 0) { | |
| 741 this.pageDiv_.appendChild( | |
| 742 this.createPageNav_(0, localStrings.getString('newest'))); | |
| 743 this.pageDiv_.appendChild( | |
| 744 this.createPageNav_(this.pageIndex_ - 1, | |
| 745 localStrings.getString('newer'))); | |
| 746 } | |
| 747 | |
| 748 // TODO(feldstein): this causes the navbar to not show up when your first | |
| 749 // page has the exact amount of results as RESULTS_PER_PAGE. | |
| 750 if (this.model_.getSize() > (this.pageIndex_ + 1) * RESULTS_PER_PAGE) { | |
| 751 this.pageDiv_.appendChild( | |
| 752 this.createPageNav_(this.pageIndex_ + 1, | |
| 753 localStrings.getString('older'))); | |
| 754 } | |
| 755 }; | |
| 756 | |
| 757 /** | |
| 758 * Make a DOM object representation of a page navigation link. | |
| 759 * @param {number} page The page index the navigation element should link to | |
| 760 * @param {string} name The text content of the link | |
| 761 * @return {HTMLAnchorElement} the pagination link | |
| 762 */ | |
| 763 HistoryView.prototype.createPageNav_ = function(page, name) { | |
| 764 anchor = document.createElement('a'); | |
| 765 anchor.className = 'page-navigation'; | |
| 766 anchor.textContent = name; | |
| 767 var hashString = PageState.getHashString(this.model_.getEditMode(), | |
| 768 this.model_.getSearchText(), page); | |
| 769 var link = 'chrome://history2/' + (hashString ? '#' + hashString : ''); | |
| 770 anchor.href = link; | |
| 771 anchor.onclick = function() { | |
| 772 setPage(page); | |
| 773 return false; | |
| 774 }; | |
| 775 return anchor; | |
| 776 }; | |
| 777 | |
| 778 /** | |
| 779 * Updates the CSS rule for the entry anchor. | |
| 780 * @private | |
| 781 */ | |
| 782 HistoryView.prototype.updateEntryAnchorWidth_ = function() { | |
| 783 // We need to have at least on .title div to be able to calculate the | |
| 784 // desired width of the anchor. | |
| 785 var titleElement = document.querySelector('.entry .title'); | |
| 786 if (!titleElement) | |
| 787 return; | |
| 788 | |
| 789 // Create new CSS rules and add them last to the last stylesheet. | |
| 790 // TODO(jochen): The following code does not work due to WebKit bug #32309 | |
| 791 // if (!this.entryAnchorRule_) { | |
| 792 // var styleSheets = document.styleSheets; | |
| 793 // var styleSheet = styleSheets[styleSheets.length - 1]; | |
| 794 // var rules = styleSheet.cssRules; | |
| 795 // var createRule = function(selector) { | |
| 796 // styleSheet.insertRule(selector + '{}', rules.length); | |
| 797 // return rules[rules.length - 1]; | |
| 798 // }; | |
| 799 // this.entryAnchorRule_ = createRule('.entry .title > a'); | |
| 800 // // The following rule needs to be more specific to have higher priority. | |
| 801 // this.entryAnchorStarredRule_ = createRule('.entry .title.starred > a'); | |
| 802 // } | |
| 803 // | |
| 804 // var anchorMaxWith = titleElement.offsetWidth; | |
| 805 // this.entryAnchorRule_.style.maxWidth = anchorMaxWith + 'px'; | |
| 806 // // Adjust by the width of star plus its margin. | |
| 807 // this.entryAnchorStarredRule_.style.maxWidth = anchorMaxWith - 23 + 'px'; | |
| 808 }; | |
| 809 | |
| 810 /////////////////////////////////////////////////////////////////////////////// | |
| 811 // State object: | |
| 812 /** | |
| 813 * An 'AJAX-history' implementation. | |
| 814 * @param {HistoryModel} model The model we're representing | |
| 815 * @param {HistoryView} view The view we're representing | |
| 816 */ | |
| 817 function PageState(model, view) { | |
| 818 // Enforce a singleton. | |
| 819 if (PageState.instance) { | |
| 820 return PageState.instance; | |
| 821 } | |
| 822 | |
| 823 this.model = model; | |
| 824 this.view = view; | |
| 825 | |
| 826 if (typeof this.checker_ != 'undefined' && this.checker_) { | |
| 827 clearInterval(this.checker_); | |
| 828 } | |
| 829 | |
| 830 // TODO(glen): Replace this with a bound method so we don't need | |
| 831 // public model and view. | |
| 832 this.checker_ = setInterval((function(state_obj) { | |
| 833 var hashData = state_obj.getHashData(); | |
| 834 | |
| 835 if (hashData.q != state_obj.model.getSearchText(term)) { | |
| 836 state_obj.view.setSearch(hashData.q, parseInt(hashData.p, 10)); | |
| 837 } else if (parseInt(hashData.p, 10) != state_obj.view.getPage()) { | |
| 838 state_obj.view.setPage(hashData.p); | |
| 839 } | |
| 840 }), 50, this); | |
| 841 } | |
| 842 | |
| 843 PageState.instance = null; | |
| 844 | |
| 845 /** | |
| 846 * @return {Object} An object containing parameters from our window hash. | |
| 847 */ | |
| 848 PageState.prototype.getHashData = function() { | |
| 849 var result = { | |
| 850 e : 0, | |
| 851 q : '', | |
| 852 p : 0 | |
| 853 }; | |
| 854 | |
| 855 if (!window.location.hash) { | |
| 856 return result; | |
| 857 } | |
| 858 | |
| 859 var hashSplit = window.location.hash.substr(1).split('&'); | |
| 860 for (var i = 0; i < hashSplit.length; i++) { | |
| 861 var pair = hashSplit[i].split('='); | |
| 862 if (pair.length > 1) { | |
| 863 result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' ')); | |
| 864 } | |
| 865 } | |
| 866 | |
| 867 return result; | |
| 868 }; | |
| 869 | |
| 870 /** | |
| 871 * Set the hash to a specified state, this will create an entry in the | |
| 872 * session history so the back button cycles through hash states, which | |
| 873 * are then picked up by our listener. | |
| 874 * @param {string} term The current search string. | |
| 875 * @param {string} page The page currently being viewed. | |
| 876 */ | |
| 877 PageState.prototype.setUIState = function(editMode, term, page) { | |
| 878 // Make sure the form looks pretty. | |
| 879 document.forms[0].term.value = term; | |
| 880 var currentHash = this.getHashData(); | |
| 881 if (Boolean(currentHash.e) != editMode || currentHash.q != term || | |
| 882 currentHash.p != page) { | |
| 883 window.location.hash = PageState.getHashString(editMode, term, page); | |
| 884 } | |
| 885 }; | |
| 886 | |
| 887 /** | |
| 888 * Static method to get the hash string for a specified state | |
| 889 * @param {string} term The current search string. | |
| 890 * @param {string} page The page currently being viewed. | |
| 891 * @return {string} The string to be used in a hash. | |
| 892 */ | |
| 893 PageState.getHashString = function(editMode, term, page) { | |
| 894 var newHash = []; | |
| 895 if (editMode) { | |
| 896 newHash.push('e=1'); | |
| 897 } | |
| 898 if (term) { | |
| 899 newHash.push('q=' + encodeURIComponent(term)); | |
| 900 } | |
| 901 if (page != undefined) { | |
| 902 newHash.push('p=' + page); | |
| 903 } | |
| 904 | |
| 905 return newHash.join('&'); | |
| 906 }; | |
| 907 | |
| 908 /////////////////////////////////////////////////////////////////////////////// | |
| 909 // Document Functions: | |
| 910 /** | |
| 911 * Window onload handler, sets up the page. | |
| 912 */ | |
| 913 function load() { | |
| 914 $('term').focus(); | |
| 915 | |
| 916 localStrings = new LocalStrings(); | |
| 917 historyModel = new HistoryModel(); | |
| 918 historyView = new HistoryView(historyModel); | |
| 919 pageState = new PageState(historyModel, historyView); | |
| 920 | |
| 921 // Create default view. | |
| 922 var hashData = pageState.getHashData(); | |
| 923 if (Boolean(hashData.e)) { | |
| 924 historyView.toggleEditMode(); | |
| 925 } | |
| 926 historyView.setSearch(hashData.q, hashData.p); | |
| 927 } | |
| 928 | |
| 929 /** | |
| 930 * TODO(glen): Get rid of this function. | |
| 931 * Set the history view to a specified page. | |
| 932 * @param {String} term The string to search for | |
| 933 */ | |
| 934 function setSearch(term) { | |
| 935 if (historyView) { | |
| 936 historyView.setSearch(term); | |
| 937 } | |
| 938 } | |
| 939 | |
| 940 /** | |
| 941 * TODO(glen): Get rid of this function. | |
| 942 * Set the history view to a specified page. | |
| 943 * @param {number} page The page to set the view to. | |
| 944 */ | |
| 945 function setPage(page) { | |
| 946 if (historyView) { | |
| 947 historyView.setPage(page); | |
| 948 } | |
| 949 } | |
| 950 | |
| 951 /** | |
| 952 * TODO(glen): Get rid of this function. | |
| 953 * Toggles edit mode. | |
| 954 */ | |
| 955 function toggleEditMode() { | |
| 956 if (historyView) { | |
| 957 historyView.toggleEditMode(); | |
| 958 historyView.reload(); | |
| 959 } | |
| 960 } | |
| 961 | |
| 962 /** | |
| 963 * Delete the next item in our deletion queue. | |
| 964 */ | |
| 965 function deleteNextInQueue() { | |
| 966 if (!deleteInFlight && deleteQueue.length) { | |
| 967 deleteInFlight = true; | |
| 968 chrome.send('removeURLsOnOneDay', | |
| 969 [String(deleteQueue[0])].concat(deleteQueue[1])); | |
| 970 } | |
| 971 } | |
| 972 | |
| 973 /** | |
| 974 * Open the clear browsing data dialog. | |
| 975 */ | |
| 976 function openClearBrowsingData() { | |
| 977 chrome.send('clearBrowsingData', []); | |
| 978 return false; | |
| 979 } | |
| 980 | |
| 981 /** | |
| 982 * Collect IDs from checked checkboxes and send to Chrome for deletion. | |
| 983 */ | |
| 984 function removeItems() { | |
| 985 var checkboxes = document.getElementsByTagName('input'); | |
| 986 var ids = []; | |
| 987 var disabledItems = []; | |
| 988 var queue = []; | |
| 989 var date = new Date(); | |
| 990 for (var i = 0; i < checkboxes.length; i++) { | |
| 991 if (checkboxes[i].type == 'checkbox' && checkboxes[i].checked && | |
| 992 !checkboxes[i].disabled) { | |
| 993 var cbDate = new Date(checkboxes[i].time); | |
| 994 if (date.getFullYear() != cbDate.getFullYear() || | |
| 995 date.getMonth() != cbDate.getMonth() || | |
| 996 date.getDate() != cbDate.getDate()) { | |
| 997 if (ids.length > 0) { | |
| 998 queue.push(date.valueOf() / 1000); | |
| 999 queue.push(ids); | |
| 1000 } | |
| 1001 ids = []; | |
| 1002 date = cbDate; | |
| 1003 } | |
| 1004 var link = $('id-' + checkboxes[i].name); | |
| 1005 checkboxes[i].disabled = true; | |
| 1006 link.style.textDecoration = 'line-through'; | |
| 1007 disabledItems.push(checkboxes[i]); | |
| 1008 ids.push(link.href); | |
| 1009 } | |
| 1010 } | |
| 1011 if (ids.length > 0) { | |
| 1012 queue.push(date.valueOf() / 1000); | |
| 1013 queue.push(ids); | |
| 1014 } | |
| 1015 if (queue.length > 0) { | |
| 1016 if (confirm(localStrings.getString('deletewarning'))) { | |
| 1017 deleteQueue = deleteQueue.concat(queue); | |
| 1018 deleteNextInQueue(); | |
| 1019 } else { | |
| 1020 // If the remove is cancelled, return the checkboxes to their | |
| 1021 // enabled, non-line-through state. | |
| 1022 for (var i = 0; i < disabledItems.length; i++) { | |
| 1023 var link = $('id-' + disabledItems[i].name); | |
| 1024 disabledItems[i].disabled = false; | |
| 1025 link.style.textDecoration = ''; | |
| 1026 } | |
| 1027 } | |
| 1028 } | |
| 1029 return false; | |
| 1030 } | |
| 1031 | |
| 1032 /** | |
| 1033 * Toggle state of checkbox and handle Shift modifier. | |
| 1034 */ | |
| 1035 function checkboxClicked(event) { | |
| 1036 if (event.shiftKey && (selectionAnchor != -1)) { | |
| 1037 var checked = this.checked; | |
| 1038 // Set all checkboxes from the anchor up to the clicked checkbox to the | |
| 1039 // state of the clicked one. | |
| 1040 var begin = Math.min(this.name, selectionAnchor); | |
| 1041 var end = Math.max(this.name, selectionAnchor); | |
| 1042 for (var i = begin; i <= end; i++) { | |
| 1043 idToCheckbox[i].checked = checked; | |
| 1044 } | |
| 1045 } | |
| 1046 selectionAnchor = this.name; | |
| 1047 this.focus(); | |
| 1048 } | |
| 1049 | |
| 1050 /////////////////////////////////////////////////////////////////////////////// | |
| 1051 // Chrome callbacks: | |
| 1052 /** | |
| 1053 * Our history system calls this function with results from searches. | |
| 1054 */ | |
| 1055 function historyResult(info, results) { | |
| 1056 historyModel.addResults(info, results); | |
| 1057 } | |
| 1058 | |
| 1059 /** | |
| 1060 * Our history system calls this function when a deletion has finished. | |
| 1061 */ | |
| 1062 function deleteComplete() { | |
| 1063 window.console.log('Delete complete'); | |
| 1064 deleteInFlight = false; | |
| 1065 if (deleteQueue.length > 2) { | |
| 1066 deleteQueue = deleteQueue.slice(2); | |
| 1067 deleteNextInQueue(); | |
| 1068 } else { | |
| 1069 deleteQueue = []; | |
| 1070 } | |
| 1071 } | |
| 1072 | |
| 1073 /** | |
| 1074 * Our history system calls this function if a delete is not ready (e.g. | |
| 1075 * another delete is in-progress). | |
| 1076 */ | |
| 1077 function deleteFailed() { | |
| 1078 window.console.log('Delete failed'); | |
| 1079 // The deletion failed - try again later. | |
| 1080 deleteInFlight = false; | |
| 1081 setTimeout(deleteNextInQueue, 500); | |
| 1082 } | |
| 1083 | |
| 1084 /** | |
| 1085 * We're called when something is deleted (either by us or by someone | |
| 1086 * else). | |
| 1087 */ | |
| 1088 function historyDeleted() { | |
| 1089 window.console.log('History deleted'); | |
| 1090 var anyChecked = document.querySelector('.entry input:checked') != null; | |
| 1091 if (!(historyView.getEditMode() && anyChecked)) | |
| 1092 historyView.reload(); | |
| 1093 } | |
| 1094 </script> | |
| 1095 <link rel="stylesheet" href="webui2.css"> | 11 <link rel="stylesheet" href="webui2.css"> |
| 1096 <style> | 12 <style> |
| 1097 #results-separator { | 13 #results-separator { |
| 1098 margin-top:12px; | 14 margin-top:12px; |
| 1099 border-top:1px solid #9cc2ef; | 15 border-top:1px solid #9cc2ef; |
| 1100 background-color:#ebeff9; | 16 background-color:#ebeff9; |
| 1101 font-weight:bold; | 17 font-weight:bold; |
| 1102 padding:3px; | 18 padding:3px; |
| 1103 margin-bottom:-8px; | 19 margin-bottom:-8px; |
| 1104 } | 20 } |
| (...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1210 color: #11c; | 126 color: #11c; |
| 1211 text-decoration: none; | 127 text-decoration: none; |
| 1212 } | 128 } |
| 1213 .entry .title > a:hover { | 129 .entry .title > a:hover { |
| 1214 text-decoration: underline; | 130 text-decoration: underline; |
| 1215 } | 131 } |
| 1216 /* Since all history links are visited, we can make them blue. */ | 132 /* Since all history links are visited, we can make them blue. */ |
| 1217 .entry .title > a:visted { | 133 .entry .title > a:visted { |
| 1218 color: #11c; | 134 color: #11c; |
| 1219 } | 135 } |
| 1220 | |
| 1221 </style> | 136 </style> |
| 1222 </head> | 137 </head> |
| 1223 <body onload="load();" i18n-values=".style.fontFamily:fontfamily;.style.fontSize
:fontsize"> | 138 <body i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize"> |
| 1224 <div class="header"> | 139 <div class="header"> |
| 1225 <a href="" onclick="setSearch(''); return false;"> | 140 <a id="history-section" href=""> |
| 1226 <img src="shared/images/history_section.png" | 141 <img src="shared/images/history_section.png" |
| 1227 width="67" height="67" class="logo" border="0"></a> | 142 width="67" height="67" class="logo" border="0"></a> |
| 1228 <form method="post" action="" | 143 <form id="search-form" method="post" action="" class="form"> |
| 1229 onsubmit="setSearch(this.term.value); return false;" | |
| 1230 class="form"> | |
| 1231 <input type="text" name="term" id="term"> | 144 <input type="text" name="term" id="term"> |
| 1232 <input type="submit" name="submit" i18n-values="value:searchbutton"> | 145 <input type="submit" name="submit" i18n-values="value:searchbutton"> |
| 1233 </form> | 146 </form> |
| 1234 </div> | 147 </div> |
| 1235 <div class="main"> | 148 <div class="main"> |
| 1236 <div id="results-separator"> | 149 <div id="results-separator"> |
| 1237 <table border="0" cellPadding="0" cellSpacing="0"> | 150 <table border="0" cellPadding="0" cellSpacing="0"> |
| 1238 <tr><td id="results-summary"></td><td id="edit-button"><p></p></td></tr> | 151 <tr><td id="results-summary"></td><td id="edit-button"><p></p></td></tr> |
| 1239 </table> | 152 </table> |
| 1240 </div> | 153 </div> |
| 1241 <div id="editing-controls"></div> | 154 <div id="editing-controls"></div> |
| 1242 <div id="results-display"></div> | 155 <div id="results-display"></div> |
| 1243 <div id="results-pagination"></div> | 156 <div id="results-pagination"></div> |
| 1244 </div> | 157 </div> |
| 1245 <div class="footer"> | 158 <div class="footer"> |
| 1246 </div> | 159 </div> |
| 160 <script src="chrome://resources/js/i18n_template.js"></script> |
| 161 <script src="chrome://resources/js/i18n_process.js"></script> |
| 1247 </body> | 162 </body> |
| 1248 </html> | 163 </html> |
| OLD | NEW |