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
5var localStrings = new LocalStrings();
6var hasPDFPlugin = true;
7
8// The total page count of the previewed document regardless of which pages the
9// user has selected.
10var 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.
15var previouslySelectedPages = [];
16
17// Timer id of the page range textfield. It is used to reset the timer whenever
18// needed.
19var timerId;
20
21/**
22 * Window onload handler, sets up the page and starts print preview by getting
23 * the printer list.
24 */
25function 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 */
55function 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 */
76function 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 */
99function 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 */
114function 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 */
127function handleCopiesFieldBlur() {
128  checkAndSetCopiesField();
129  printSettingChanged();
130}
131
132/**
133 * Handles page ranges field blur event.
134 */
135function 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 */
146function 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 */
161function 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 */
179function 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 */
201function 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 */
210function 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 */
219function 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 */
231function 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 */
243function 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 */
252function 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 */
275function printFile() {
276  chrome.send('print', [getSettingsJSON()]);
277}
278
279/**
280 * Asks the browser to generate a preview PDF based on current print settings.
281 */
282function 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 */
292function 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 */
319function 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 */
329function 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 */
344function 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 */
367function 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 */
412function updatePrintButtonState() {
413  $('print-button').disabled = (!($('all-pages').checked ||
414                                  $('individual-pages').checkValidity()) ||
415                                !$('copies').checkValidity());
416}
417
418window.addEventListener('DOMContentLoaded', onLoad);
419
420/**
421 * Listener function that executes whenever any of the available settings
422 * is changed.
423 */
424function 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 */
433function 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 */
501function 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 */
510function 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 */
521function 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 */
535function 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 */
572function 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 */
589function 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 */
597function 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 */
608function 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 */
623function 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