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 var localStrings = new LocalStrings(); 6 var hasPDFPlugin = true; 7 8 // The total page count of the previewed document regardless of which pages the 9 // user has selected. 10 var totalPageCount = -1; 11 12 // The previously selected pages by the user. It is used in 13 // onPageSelectionMayHaveChanged() to make sure that a new preview is not 14 // requested more often than necessary. 15 var previouslySelectedPages = []; 16 17 // Timer id of the page range textfield. It is used to reset the timer whenever 18 // needed. 19 var timerId; 20 21 /** 22 * Window onload handler, sets up the page and starts print preview by getting 23 * the printer list. 24 */ 25 function onLoad() { 26 initializeAnimation(); 27 28 $('printer-list').disabled = true; 29 $('print-button').disabled = true; 30 $('print-button').addEventListener('click', printFile); 31 $('cancel-button').addEventListener('click', function(e) { 32 window.close(); 33 }); 34 35 $('all-pages').addEventListener('click', onPageSelectionMayHaveChanged); 36 $('copies').addEventListener('input', validateNumberOfCopies); 37 $('copies').addEventListener('blur', handleCopiesFieldBlur); 38 $('print-pages').addEventListener('click', handleIndividualPagesCheckbox); 39 $('individual-pages').addEventListener('blur', handlePageRangesFieldBlur); 40 $('individual-pages').addEventListener('focus', addTimerToPageRangeField); 41 $('individual-pages').addEventListener('input', resetPageRangeFieldTimer); 42 $('landscape').addEventListener('click', onLayoutModeToggle); 43 $('portrait').addEventListener('click', onLayoutModeToggle); 44 $('color').addEventListener('click', function() { setColor(true); }); 45 $('bw').addEventListener('click', function() { setColor(false); }); 46 $('printer-list').addEventListener( 47 'change', updateControlsWithSelectedPrinterCapabilities); 48 49 chrome.send('getPrinters'); 50 } 51 52 /** 53 * Gets the selected printer capabilities and updates the controls accordingly. 54 */ 55 function updateControlsWithSelectedPrinterCapabilities() { 56 var printerList = $('printer-list'); 57 var selectedPrinter = printerList.selectedIndex; 58 if (selectedPrinter < 0) 59 return; 60 61 var printerName = printerList.options[selectedPrinter].textContent; 62 if (printerName == localStrings.getString('printToPDF')) { 63 updateWithPrinterCapabilities({'disableColorOption': true, 64 'setColorAsDefault': true}); 65 } else { 66 // This message will call back to 'updateWithPrinterCapabilities' 67 // function. 68 chrome.send('getPrinterCapabilities', [printerName]); 69 } 70 } 71 72 /** 73 * Updates the controls with printer capabilities information. 74 * @param {Object} settingInfo printer setting information. 75 */ 76 function updateWithPrinterCapabilities(settingInfo) { 77 var disableColorOption = settingInfo.disableColorOption; 78 var setColorAsDefault = settingInfo.setColorAsDefault; 79 var colorOption = $('color'); 80 var bwOption = $('bw'); 81 82 if (disableColorOption != colorOption.disabled) { 83 setControlAndLabelDisabled(colorOption, disableColorOption); 84 setControlAndLabelDisabled(bwOption, disableColorOption); 85 } 86 87 if (colorOption.checked != setColorAsDefault) { 88 colorOption.checked = setColorAsDefault; 89 bwOption.checked = !setColorAsDefault; 90 setColor(colorOption.checked); 91 } 92 } 93 94 /** 95 * Disables the input control element and its associated label. 96 * @param {HTMLElement} controlElm An input control element. 97 * @param {boolean} disable set to true to disable element and label. 98 */ 99 function setControlAndLabelDisabled(controlElm, disable) { 100 controlElm.disabled = disable; 101 var label = $(controlElm.getAttribute('label')); 102 if (disable) 103 label.classList.add('disabled-label-text'); 104 else 105 label.classList.remove('disabled-label-text'); 106 } 107 108 /** 109 * Parses the copies field text for validation and updates the state of print 110 * button and collate checkbox. If the specified value is invalid, displays an 111 * invalid warning icon on the text box and sets the error message as the title 112 * message of text box. 113 */ 114 function validateNumberOfCopies() { 115 var copiesField = $('copies'); 116 var message = ''; 117 if (!isNumberOfCopiesValid()) 118 message = localStrings.getString('invalidNumberOfCopiesTitleToolTip'); 119 copiesField.setCustomValidity(message); 120 copiesField.title = message; 121 updatePrintButtonState(); 122 } 123 124 /** 125 * Handles copies field blur event. 126 */ 127 function handleCopiesFieldBlur() { 128 checkAndSetCopiesField(); 129 printSettingChanged(); 130 } 131 132 /** 133 * Handles page ranges field blur event. 134 */ 135 function handlePageRangesFieldBlur() { 136 checkAndSetPageRangesField(); 137 onPageSelectionMayHaveChanged(); 138 } 139 140 /** 141 * Validates the copies text field value. 142 * NOTE: An empty copies field text is considered valid because the blur event 143 * listener of this field will set it back to a default value. 144 * @return {boolean} true if the number of copies is valid else returns false. 145 */ 146 function isNumberOfCopiesValid() { 147 var copiesFieldText = $('copies').value.replace(/\s/g, ''); 148 if (copiesFieldText == '') 149 return true; 150 151 var numericExp = /^[0-9]+$/; 152 return (numericExp.test(copiesFieldText) && Number(copiesFieldText) > 0); 153 } 154 155 /** 156 * Checks the value of the copies field. If it is a valid number it does 157 * nothing. If it can only parse the first part of the string it replaces the 158 * string with the first part. Example: '123abcd' becomes '123'. 159 * If the string can't be parsed at all it replaces with 1. 160 */ 161 function checkAndSetCopiesField() { 162 var copiesField = $('copies'); 163 var copies = parseInt(copiesField.value, 10); 164 if (isNaN(copies)) 165 copies = 1; 166 copiesField.value = copies; 167 updateSummary(); 168 } 169 170 /** 171 * Checks the value of the page ranges text field. It parses the page ranges and 172 * normalizes them. For example: '1,2,3,5,9-10' becomes '1-3, 5, 9-10'. 173 * If it can't parse the whole string it will replace with the part it parsed. 174 * For example: '1-6,9-10,sd343jf' becomes '1-6, 9-10'. If the specified page 175 * range includes all pages it replaces it with the empty string (so that the 176 * example text is automatically shown. 177 * 178 */ 179 function checkAndSetPageRangesField() { 180 var pageRanges = getSelectedPageRanges(); 181 var parsedPageRanges = ''; 182 var individualPagesField = $('individual-pages'); 183 184 for (var i = 0; i < pageRanges.length; ++i) { 185 if (pageRanges[i].from == pageRanges[i].to) 186 parsedPageRanges += pageRanges[i].from; 187 else 188 parsedPageRanges += pageRanges[i].from + '-' + pageRanges[i].to; 189 if (i < pageRanges.length - 1) 190 parsedPageRanges += ', '; 191 } 192 individualPagesField.value = parsedPageRanges; 193 updateSummary(); 194 } 195 196 /** 197 * Checks whether the preview layout setting is set to 'landscape' or not. 198 * 199 * @return {boolean} true if layout is 'landscape'. 200 */ 201 function isLandscape() { 202 return $('landscape').checked; 203 } 204 205 /** 206 * Checks whether the preview color setting is set to 'color' or not. 207 * 208 * @return {boolean} true if color is 'color'. 209 */ 210 function isColor() { 211 return $('color').checked; 212 } 213 214 /** 215 * Checks whether the preview collate setting value is set or not. 216 * 217 * @return {boolean} true if collate setting is enabled and checked. 218 */ 219 function isCollated() { 220 var collateField = $('collate'); 221 return !collateField.disabled && collateField.checked; 222 } 223 224 /** 225 * Returns the number of copies currently indicated in the copies textfield. If 226 * the contents of the textfield can not be converted to a number or if <1 it 227 * returns 1. 228 * 229 * @return {number} number of copies. 230 */ 231 function getCopies() { 232 var copies = parseInt($('copies').value, 10); 233 if (!copies || copies <= 1) 234 copies = 1; 235 return copies; 236 } 237 238 /** 239 * Checks whether the preview two-sided checkbox is checked. 240 * 241 * @return {boolean} true if two-sided is checked. 242 */ 243 function isTwoSided() { 244 return $('two-sided').checked; 245 } 246 247 /** 248 * Creates a JSON string based on the values in the printer settings. 249 * 250 * @return {string} JSON string with print job settings. 251 */ 252 function getSettingsJSON() { 253 var printerList = $('printer-list') 254 var selectedPrinter = printerList.selectedIndex; 255 var printerName = ''; 256 if (selectedPrinter >= 0) 257 printerName = printerList.options[selectedPrinter].textContent; 258 var printAll = $('all-pages').checked; 259 var printToPDF = (printerName == localStrings.getString('printToPDF')); 260 261 return JSON.stringify({'printerName': printerName, 262 'pageRange': getSelectedPageRanges(), 263 'printAll': printAll, 264 'twoSided': isTwoSided(), 265 'copies': getCopies(), 266 'collate': isCollated(), 267 'landscape': isLandscape(), 268 'color': isColor(), 269 'printToPDF': printToPDF}); 270 } 271 272 /** 273 * Asks the browser to print the preview PDF based on current print settings. 274 */ 275 function printFile() { 276 chrome.send('print', [getSettingsJSON()]); 277 } 278 279 /** 280 * Asks the browser to generate a preview PDF based on current print settings. 281 */ 282 function getPreview() { 283 chrome.send('getPreview', [getSettingsJSON()]); 284 } 285 286 /** 287 * Fill the printer list drop down. 288 * Called from PrintPreviewHandler::SendPrinterList(). 289 * @param {Array} printers Array of printer names. 290 * @param {number} defaultPrinterIndex The index of the default printer. 291 */ 292 function setPrinters(printers, defaultPrinterIndex) { 293 var printerList = $('printer-list'); 294 for (var i = 0; i < printers.length; ++i) { 295 var option = document.createElement('option'); 296 option.textContent = printers[i]; 297 printerList.add(option); 298 if (i == defaultPrinterIndex) 299 option.selected = true; 300 } 301 302 // Adding option for saving PDF to disk. 303 var option = document.createElement('option'); 304 option.textContent = localStrings.getString('printToPDF'); 305 printerList.add(option); 306 printerList.disabled = false; 307 308 updateControlsWithSelectedPrinterCapabilities(); 309 310 // Once the printer list is populated, generate the initial preview. 311 getPreview(); 312 } 313 314 /** 315 * Sets the color mode for the PDF plugin. 316 * Called from PrintPreviewHandler::ProcessColorSetting(). 317 * @param {boolean} color is true if the PDF plugin should display in color. 318 */ 319 function setColor(color) { 320 if (!hasPDFPlugin) { 321 return; 322 } 323 $('pdf-viewer').grayscale(!color); 324 } 325 326 /** 327 * Called when the PDF plugin loads its document. 328 */ 329 function onPDFLoad() { 330 if (isLandscape()) 331 $('pdf-viewer').fitToWidth(); 332 else 333 $('pdf-viewer').fitToHeight(); 334 } 335 336 /** 337 * Update the print preview when new preview data is available. 338 * Create the PDF plugin as needed. 339 * Called from PrintPreviewUI::PreviewDataIsAvailable(). 340 * @param {number} pageCount The expected total pages count. 341 * @param {string} jobTitle The print job title. 342 * 343 */ 344 function updatePrintPreview(pageCount, jobTitle) { 345 // Initialize the expected page count. 346 if (totalPageCount == -1) 347 totalPageCount = pageCount; 348 349 // Initialize the selected pages (defaults to all selected). 350 if (previouslySelectedPages.length == 0) 351 for (var i = 0; i < totalPageCount; i++) 352 previouslySelectedPages.push(i+1); 353 354 regeneratePreview = false; 355 356 // Update the current tab title. 357 document.title = localStrings.getStringF('printPreviewTitleFormat', jobTitle); 358 359 createPDFPlugin(); 360 361 updateSummary(); 362 } 363 364 /** 365 * Create the PDF plugin or reload the existing one. 366 */ 367 function createPDFPlugin() { 368 if (!hasPDFPlugin) { 369 return; 370 } 371 372 // Enable the print button. 373 if (!$('printer-list').disabled) { 374 $('print-button').disabled = false; 375 } 376 377 var pdfViewer = $('pdf-viewer'); 378 if (pdfViewer) { 379 pdfViewer.reload(); 380 pdfViewer.grayscale(!isColor()); 381 return; 382 } 383 384 var loadingElement = $('loading'); 385 loadingElement.classList.add('hidden'); 386 var mainView = loadingElement.parentNode; 387 388 var pdfPlugin = document.createElement('embed'); 389 pdfPlugin.setAttribute('id', 'pdf-viewer'); 390 pdfPlugin.setAttribute('type', 'application/pdf'); 391 pdfPlugin.setAttribute('src', 'chrome://print/print.pdf'); 392 mainView.appendChild(pdfPlugin); 393 if (!pdfPlugin.onload) { 394 hasPDFPlugin = false; 395 mainView.removeChild(pdfPlugin); 396 $('no-plugin').classList.remove('hidden'); 397 return; 398 } 399 pdfPlugin.grayscale(true); 400 pdfPlugin.onload('onPDFLoad()'); 401 } 402 403 /** 404 * Updates the state of print button depending on the user selection. 405 * 406 * If the user has selected 'All' pages option, enables the print button. 407 * If the user has selected a page range, depending on the validity of page 408 * range text enables/disables the print button. 409 * Depending on the validity of 'copies' value, enables/disables the print 410 * button. 411 */ 412 function updatePrintButtonState() { 413 $('print-button').disabled = (!($('all-pages').checked || 414 $('individual-pages').checkValidity()) || 415 !$('copies').checkValidity()); 416 } 417 418 window.addEventListener('DOMContentLoaded', onLoad); 419 420 /** 421 * Listener function that executes whenever any of the available settings 422 * is changed. 423 */ 424 function printSettingChanged() { 425 $('collate-option').hidden = getCopies() <= 1; 426 updateSummary(); 427 } 428 429 /** 430 * Updates the print summary based on the currently selected user options. 431 * 432 */ 433 function updateSummary() { 434 var copies = getCopies(); 435 var printButton = $('print-button'); 436 var printSummary = $('print-summary'); 437 438 if (isNaN($('copies').value)) { 439 printSummary.innerHTML = 440 localStrings.getString('invalidNumberOfCopiesTitleToolTip'); 441 return; 442 } 443 444 var pageList = getSelectedPages(); 445 if (pageList.length <= 0) { 446 printSummary.innerHTML = 447 localStrings.getString('pageRangeInvalidTitleToolTip'); 448 printButton.disabled = true; 449 return; 450 } 451 452 var pagesLabel = localStrings.getString('printPreviewPageLabelSingular'); 453 var twoSidedLabel = ''; 454 var timesSign = ''; 455 var numOfCopies = ''; 456 var copiesLabel = ''; 457 var equalSign = ''; 458 var numOfSheets = ''; 459 var sheetsLabel = ''; 460 461 printButton.disabled = false; 462 463 if (pageList.length > 1) 464 pagesLabel = localStrings.getString('printPreviewPageLabelPlural'); 465 466 if (isTwoSided()) 467 twoSidedLabel = '('+localStrings.getString('optionTwoSided')+')'; 468 469 if (copies > 1) { 470 timesSign = ''; 471 numOfCopies = copies; 472 copiesLabel = localStrings.getString('copiesLabel').toLowerCase(); 473 } 474 475 if ((copies > 1) || (isTwoSided())) { 476 numOfSheets = pageList.length; 477 478 if (isTwoSided()) 479 numOfSheets = Math.ceil(numOfSheets / 2); 480 481 equalSign = '='; 482 numOfSheets *= copies; 483 sheetsLabel = localStrings.getString('printPreviewSheetsLabel'); 484 } 485 486 var html = localStrings.getStringF('printPreviewSummaryFormat', 487 pageList.length, pagesLabel, 488 twoSidedLabel, timesSign, numOfCopies, 489 copiesLabel, equalSign, 490 '<strong>' + numOfSheets + '</strong>', 491 '<strong>' + sheetsLabel + '</strong>'); 492 493 // Removing extra spaces from within the string. 494 html.replace(/\s{2,}/g, ' '); 495 printSummary.innerHTML = html; 496 } 497 498 /** 499 * Handles a click event on the two-sided option. 500 */ 501 function handleTwoSidedClick(event) { 502 handleZippyClickEl($('binding')); 503 printSettingChanged(event); 504 } 505 506 /** 507 * Gives focus to the individual pages textfield when 'print-pages' textbox is 508 * clicked. 509 */ 510 function handleIndividualPagesCheckbox() { 511 printSettingChanged(); 512 $('individual-pages').focus(); 513 } 514 515 /** 516 * When the user switches printing orientation mode the page field selection is 517 * reset to "all pages selected". After the change the number of pages will be 518 * different and currently selected page numbers might no longer be valid. 519 * Even if they are still valid the content of these pages will be different. 520 */ 521 function onLayoutModeToggle() { 522 $('individual-pages').value = ''; 523 $('all-pages').checked = true; 524 totalPageCount = -1; 525 previouslySelectedPages.length = 0; 526 getPreview(); 527 } 528 529 /** 530 * Returns a list of all pages in the specified ranges. If the page ranges can't 531 * be parsed an empty list is returned. 532 * 533 * @return {Array} 534 */ 535 function getSelectedPages() { 536 var pageText = $('individual-pages').value; 537 538 if ($('all-pages').checked || pageText == '') 539 pageText = '1-' + totalPageCount; 540 541 var pageList = []; 542 var parts = pageText.split(/,/); 543 544 for (var i = 0; i < parts.length; ++i) { 545 var part = parts[i]; 546 var match = part.match(/([0-9]+)-([0-9]+)/); 547 548 if (match && match[1] && match[2]) { 549 var from = parseInt(match[1], 10); 550 var to = parseInt(match[2], 10); 551 552 if (from && to) { 553 for (var j = from; j <= to; ++j) 554 if (j <= totalPageCount) 555 pageList.push(j); 556 } 557 } else if (parseInt(part, 10)) { 558 if (parseInt(part, 10) <= totalPageCount) 559 pageList.push(parseInt(part, 10)); 560 } 561 } 562 return pageList; 563 } 564 565 /** 566 * Parses the selected page ranges, processes them and returns the results. 567 * It squashes whenever possible. Example '1-2,3,5-7' becomes 1-3,5-7 568 * 569 * @return {Array} an array of page range objects. A page range object has 570 * fields 'from' and 'to'. 571 */ 572 function getSelectedPageRanges() { 573 var pageList = getSelectedPages(); 574 var pageRanges = []; 575 for (var i = 0; i < pageList.length; ++i) { 576 tempFrom = pageList[i]; 577 while (i + 1 < pageList.length && pageList[i + 1] == pageList[i] + 1) 578 ++i; 579 tempTo = pageList[i]; 580 pageRanges.push({'from': tempFrom, 'to': tempTo}); 581 } 582 return pageRanges; 583 } 584 585 /** 586 * Whenever the page range textfield gains focus we add a timer to detect when 587 * the user stops typing in order to update the print preview. 588 */ 589 function addTimerToPageRangeField() { 590 timerId = window.setTimeout(onPageSelectionMayHaveChanged, 500); 591 } 592 593 /** 594 * As the user types in the page range textfield, we need to reset this timer, 595 * since the page ranges are still being edited. 596 */ 597 function resetPageRangeFieldTimer() { 598 clearTimeout(timerId); 599 addTimerToPageRangeField(); 600 } 601 602 /** 603 * When the user stops typing in the page range textfield or clicks on the 604 * 'all-pages' checkbox, a new print preview is requested, only if 605 * 1) The input is valid (it can be parsed, even only partially). 606 * 2) The newly selected pages differ from the previously selected. 607 */ 608 function onPageSelectionMayHaveChanged() { 609 var currentlySelectedPages = getSelectedPages(); 610 611 if (currentlySelectedPages.length == 0) 612 return; 613 if (areArraysEqual(previouslySelectedPages, currentlySelectedPages)) 614 return; 615 616 previouslySelectedPages = currentlySelectedPages; 617 getPreview(); 618 } 619 620 /** 621 * Returns true if the contents of the two arrays are equal. 622 */ 623 function areArraysEqual(array1, array2) { 624 if (array1.length != array2.length) 625 return false; 626 for (var i = 0; i < array1.length; i++) 627 if(array1[i] != array2[i]) 628 return false; 629 return true; 630 } 631