Bug 499640: add frontend support for non-contiguous page ranges r=mstriemer,fluent-reviewers

A comma will separate the list of ranges, and a dash indicates a page range.
If the print preview returns an empty page, we send an update event
to display all pages and invalidate the form.

This patch also changes how we create deferred tasks and instead has the
PrintEventHandler create them. Depending on the type of setting changed,
the PrintEventHandler either immediately handles the event or arms the
delayed settings change task. If the input is invalid, we cancel the settings
change and disarm the task. We finalize any pending tasks when the user prints
and recreate them in case the print was "unsuccessful," meaning the form
is now invalid or the user cnacelled saving as a pdf.

Differential Revision: https://phabricator.services.mozilla.com/D95222
This commit is contained in:
Emma Malysz 2020-12-07 21:19:16 +00:00
Родитель d9e09b568e
Коммит 90d1ea0e46
8 изменённых файлов: 625 добавлений и 265 удалений

Просмотреть файл

@ -250,25 +250,8 @@ input[type="text"] {
margin: 0;
}
.range-group-input {
width: 3em;
height: max-content;
appearance: textfield !important;
margin-inline: 5px;
}
.range-part-label {
height: max-content;
margin-top: 8px;
}
.range-group {
align-items: center;
display: inline-flex;
}
#range-picker {
margin-bottom: 8px;
#custom-range {
margin-top: 4px;
}
.vertical-margins,
@ -288,11 +271,6 @@ input[type="text"] {
margin-top: 2px;
}
input[type="number"]:invalid {
border: 1px solid #D70022;
box-shadow: 0 0 0 1px #D70022, 0 0 0 4px rgba(215, 0, 34, 0.3);
}
.toggle-group #landscape + .toggle-group-label::before {
content: url("chrome://global/content/landscape.svg");
}
@ -301,6 +279,7 @@ input[type="number"]:invalid {
}
select:invalid,
input[type="text"]:invalid,
input[type="number"]:invalid {
border: 1px solid #D70022;
box-shadow: 0 0 0 1px #D70022, 0 0 0 4px rgba(215, 0, 34, 0.3);

Просмотреть файл

@ -22,19 +22,7 @@
<option value="all" selected data-l10n-id="printui-page-range-all"></option>
<option value="custom" data-l10n-id="printui-page-range-custom"></option>
</select>
<div class="range-group" hidden>
<label data-l10n-id="printui-range-start" class="range-group-separator" for="custom-range-start"></label>
<!-- Note that here and elsewhere, we're setting aria-errormessage
attributes to a list of all possible errors. The a11y APIs will
filter this down to visible items only. -->
<input id="custom-range-start" type="number"
aria-errormessage="error-invalid-range error-invalid-start-range-overflow"
min="1" step="1" class="range-group-input" disabled>
<label data-l10n-id="printui-range-end" class="range-group-separator" for="custom-range-end"></label>
<input id="custom-range-end" type="number"
aria-errormessage="error-invalid-range error-invalid-start-range-overflow"
min="1" step="1" class="range-group-input" disabled>
</div>
<input id="custom-range" type="text" disabled hidden data-l10n-id="printui-page-custom-range-input" aria-errormessage="error-invalid-range error-invalid-start-range-overflow">
<p id="error-invalid-range" hidden data-l10n-id="printui-error-invalid-range" class="error-message" role="alert" data-l10n-args='{ "numPages": 1 }'></p>
<p id="error-invalid-start-range-overflow" hidden data-l10n-id="printui-error-invalid-start-overflow" class="error-message" role="alert"></p>
</template>

Просмотреть файл

@ -84,12 +84,6 @@ function cancelDeferredTasks() {
deferredTasks = [];
}
async function finalizeDeferredTasks() {
await Promise.all(deferredTasks.map(task => task.finalize()));
printPending = true;
await PrintEventHandler._updatePrintPreviewTask.finalize();
}
document.addEventListener(
"DOMContentLoaded",
e => {
@ -118,6 +112,8 @@ var PrintEventHandler = {
settings: null,
defaultSettings: null,
allPaperSizes: {},
previewIsEmpty: false,
_delayedChanges: {},
_nonFlaggedChangedSettings: {},
_userChangedSettings: {},
settingFlags: {
@ -219,7 +215,8 @@ var PrintEventHandler = {
);
let didPrint = await this.print();
if (!didPrint) {
// Re-enable elements of the form if the user cancels saving
// Re-enable elements of the form if the user cancels saving or
// if a deferred task rendered the page invalid.
this.printForm.enable();
}
// Reset the cancel button regardless of the outcome.
@ -228,9 +225,16 @@ var PrintEventHandler = {
cancelButton.dataset.cancelL10nId
);
});
document.addEventListener("update-print-settings", e =>
this.onUserSettingsChange(e.detail)
);
this._createDelayedSettingsChangeTask();
document.addEventListener("update-print-settings", e => {
this.handleSettingsChange(e.detail);
});
document.addEventListener("cancel-print-settings", e => {
this._delayedSettingsChangeTask.disarm();
for (let setting of Object.keys(e.detail)) {
delete this._delayedChanges[setting];
}
});
document.addEventListener("cancel-print", () => this.cancelPrint());
document.addEventListener("open-system-dialog", async () => {
// This file in only used if pref print.always_print_silent is false, so
@ -291,11 +295,7 @@ var PrintEventHandler = {
// Use a DeferredTask for updating the preview. This will ensure that we
// only have one update running at a time.
this._updatePrintPreviewTask = new DeferredTask(async () => {
await initialPreviewDone;
await this._updatePrintPreview();
document.dispatchEvent(new CustomEvent("preview-updated"));
}, 0);
this._createUpdatePrintPreviewTask(initialPreviewDone);
document.dispatchEvent(
new CustomEvent("available-destinations", {
@ -331,6 +331,24 @@ var PrintEventHandler = {
// Disable the form when a print is in progress
this.printForm.disable();
if (Object.keys(this._delayedChanges).length) {
// Make sure any pending changes get saved.
let task = this._delayedSettingsChangeTask;
this._createDelayedSettingsChangeTask();
await task.finalize();
}
if (this.settings.pageRanges.length) {
// Finish any running previews to verify the range is still valid.
let task = this._updatePrintPreviewTask;
this._createUpdatePrintPreviewTask();
await task.finalize();
}
if (!this.printForm.checkValidity() || this.previewIsEmpty) {
return false;
}
let settings = systemDialogSettings || this.settings;
if (settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER) {
@ -346,8 +364,6 @@ var PrintEventHandler = {
await window._initialized;
await finalizeDeferredTasks();
// This seems like it should be handled automatically but it isn't.
Services.prefs.setStringPref("print_printer", settings.printerName);
@ -465,6 +481,58 @@ var PrintEventHandler = {
return settingsToUpdate;
},
_createDelayedSettingsChangeTask() {
this._delayedSettingsChangeTask = createDeferredTask(async () => {
if (Object.keys(this._delayedChanges).length) {
let changes = this._delayedChanges;
this._delayedChanges = {};
await this.onUserSettingsChange(changes);
}
}, INPUT_DELAY_MS);
},
_createUpdatePrintPreviewTask(initialPreviewDone = null) {
this._updatePrintPreviewTask = new DeferredTask(async () => {
await initialPreviewDone;
await this._updatePrintPreview();
document.dispatchEvent(new CustomEvent("preview-updated"));
}, 0);
},
_scheduleDelayedSettingsChange(changes) {
Object.assign(this._delayedChanges, changes);
this._delayedSettingsChangeTask.disarm();
this._delayedSettingsChangeTask.arm();
},
handleSettingsChange(changedSettings = {}) {
let delayedChanges = {};
let instantChanges = {};
for (let [setting, value] of Object.entries(changedSettings)) {
switch (setting) {
case "pageRanges":
case "scaling":
delayedChanges[setting] = value;
break;
case "customMargins":
delete this._delayedChanges.margins;
changedSettings.margins == "custom"
? (delayedChanges[setting] = value)
: (instantChanges[setting] = value);
break;
default:
instantChanges[setting] = value;
break;
}
}
if (Object.keys(delayedChanges).length) {
this._scheduleDelayedSettingsChange(delayedChanges);
}
if (Object.keys(instantChanges).length) {
this.onUserSettingsChange(instantChanges);
}
},
async onUserSettingsChange(changedSettings = {}) {
for (let [setting, value] of Object.entries(changedSettings)) {
Services.telemetry.keyedScalarAdd(
@ -572,6 +640,14 @@ var PrintEventHandler = {
this.viewSettings[setting] != value ||
(printerChanged && setting == "paperId")
) {
if (setting == "pageRanges") {
// The page range is kept as an array. If the user switches between all
// and custom with no specified range input (which is represented as an
// empty array), we do not want to send an update.
if (!this.viewSettings[setting].length && !value.length) {
continue;
}
}
this.viewSettings[setting] = value;
if (
@ -651,19 +727,28 @@ var PrintEventHandler = {
.add(elapsed);
}
let totalPageCount, sheetCount, hasSelection;
let totalPageCount, sheetCount, hasSelection, isEmpty;
try {
// This resolves with a PrintPreviewSuccessInfo dictionary.
({
totalPageCount,
sheetCount,
hasSelection,
isEmpty,
} = await previewBrowser.frameLoader.printPreview(settings, sourceWinId));
} catch (e) {
this.reportPrintingError("PRINT_PREVIEW");
throw e;
}
this.previewIsEmpty = isEmpty;
// If the preview is empty, we know our range is greater than the number of pages.
// We have to send a pageRange update to display a non-empty page.
if (this.previewIsEmpty) {
this.viewSettings.pageRanges = [];
this.updatePrintPreview();
}
// Update the settings print options on whether there is a selection.
settings.isPrintSelectionRBEnabled = hasSelection;
@ -1339,6 +1424,15 @@ function PrintUIControlMixin(superClass) {
);
}
cancelSettingsChange(changedSettings) {
this.dispatchEvent(
new CustomEvent("cancel-print-settings", {
bubbles: true,
detail: changedSettings,
})
);
}
handleKeypress(e) {
let char = String.fromCharCode(e.charCode);
let acceptedChar = e.target.step.includes(".")
@ -1444,8 +1538,11 @@ class ColorModePicker extends PrintSettingSelect {
update(settings) {
this.value = settings[this.settingName] ? "color" : "bw";
let canSwitch = settings.supportsColor && settings.supportsMonochrome;
this.toggleAttribute("disallowed", !canSwitch);
this.disabled = !canSwitch;
if (this.disablePicker != canSwitch) {
this.toggleAttribute("disallowed", !canSwitch);
this.disabled = !canSwitch;
}
this.disablePicker = canSwitch;
}
handleEvent(e) {
@ -1572,9 +1669,50 @@ class PrintUIForm extends PrintUIControlMixin(HTMLFormElement) {
}
enable() {
for (let element of this.elements) {
if (!element.hasAttribute("disallowed")) {
element.disabled = false;
let isValid = this.checkValidity();
document.body.toggleAttribute("invalid", !isValid);
if (isValid) {
for (let element of this.elements) {
if (!element.hasAttribute("disallowed")) {
element.disabled = false;
}
}
// aria-describedby will usually cause the first value to be reported.
// Unfortunately, screen readers don't pick up description changes from
// dialogs, so we must use a live region. To avoid double reporting of
// the first value, we don't set aria-live initially. We only set it for
// subsequent updates.
// aria-live is set on the parent because sheetCount itself might be
// hidden and then shown, and updates are only reported for live
// regions that were already visible.
document
.querySelector("#sheet-count")
.parentNode.setAttribute("aria-live", "polite");
} else {
// Find the invalid element
let invalidElement;
for (let element of this.elements) {
if (!element.checkValidity()) {
invalidElement = element;
break;
}
}
let section = invalidElement.closest(".section-block");
document.body.toggleAttribute("invalid", !isValid);
// We're hiding the sheet count and aria-describedby includes the
// content of hidden elements, so remove aria-describedby.
document.body.removeAttribute("aria-describedby");
for (let element of this.elements) {
// If we're valid, enable all inputs.
// Otherwise, disable the valid inputs other than the cancel button and the elements
// in the invalid section.
element.disabled =
element.hasAttribute("disallowed") ||
(!isValid &&
element.validity.valid &&
element.name != "cancel" &&
element.closest(".section-block") != this._printerDestination &&
element.closest(".section-block") != section);
}
}
}
@ -1601,38 +1739,7 @@ class PrintUIForm extends PrintUIControlMixin(HTMLFormElement) {
e.type == "input" ||
e.type == "revalidate"
) {
let isValid = this.checkValidity();
let section = e.target.closest(".section-block");
document.body.toggleAttribute("invalid", !isValid);
if (isValid) {
// aria-describedby will usually cause the first value to be reported.
// Unfortunately, screen readers don't pick up description changes from
// dialogs, so we must use a live region. To avoid double reporting of
// the first value, we don't set aria-live initially. We only set it for
// subsequent updates.
// aria-live is set on the parent because sheetCount itself might be
// hidden and then shown, and updates are only reported for live
// regions that were already visible.
document
.querySelector("#sheet-count")
.parentNode.setAttribute("aria-live", "polite");
} else {
// We're hiding the sheet count and aria-describedby includes the
// content of hidden elements, so remove aria-describedby.
document.body.removeAttribute("aria-describedby");
}
for (let element of this.elements) {
// If we're valid, enable all inputs.
// Otherwise, disable the valid inputs other than the cancel button and the elements
// in the invalid section.
element.disabled =
element.hasAttribute("disallowed") ||
(!isValid &&
element.validity.valid &&
element.name != "cancel" &&
element.closest(".section-block") != this._printerDestination &&
element.closest(".section-block") != section);
}
this.enable();
}
}
}
@ -1655,11 +1762,6 @@ class ScaleInput extends PrintUIControlMixin(HTMLElement) {
this._percentScale.addEventListener("keypress", this);
this._percentScale.addEventListener("paste", this);
this.addEventListener("input", this);
this._updateScaleTask = createDeferredTask(
() => this.updateScale(),
INPUT_DELAY_MS
);
}
updateScale() {
@ -1672,8 +1774,11 @@ class ScaleInput extends PrintUIControlMixin(HTMLElement) {
let { scaling, shrinkToFit, printerName } = settings;
this._shrinkToFitChoice.checked = shrinkToFit;
this._scaleChoice.checked = !shrinkToFit;
this._percentScale.disabled = shrinkToFit;
this._percentScale.toggleAttribute("disallowed", shrinkToFit);
if (this.disableScale != shrinkToFit) {
this._percentScale.disabled = shrinkToFit;
this._percentScale.toggleAttribute("disallowed", shrinkToFit);
}
this.disableScale = shrinkToFit;
if (!this.printerName) {
this.printerName = printerName;
}
@ -1711,8 +1816,6 @@ class ScaleInput extends PrintUIControlMixin(HTMLElement) {
this.handlePaste(e);
}
this._updateScaleTask.disarm();
if (e.target == this._shrinkToFitChoice || e.target == this._scaleChoice) {
if (!this._percentScale.checkValidity()) {
this._percentScale.value = 100;
@ -1728,7 +1831,7 @@ class ScaleInput extends PrintUIControlMixin(HTMLElement) {
this._scaleError.hidden = true;
} else if (e.type == "input") {
if (this._percentScale.checkValidity()) {
this._updateScaleTask.arm();
this.updateScale();
}
}
@ -1736,6 +1839,7 @@ class ScaleInput extends PrintUIControlMixin(HTMLElement) {
if (this._percentScale.validity.valid) {
this._scaleError.hidden = true;
} else {
this.cancelSettingsChange({ scaling: true });
this.showErrorTimeoutId = window.setTimeout(() => {
this._scaleError.hidden = false;
}, INPUT_DELAY_MS);
@ -1748,18 +1852,15 @@ class PageRangeInput extends PrintUIControlMixin(HTMLElement) {
initialize() {
super.initialize();
this._startRange = this.querySelector("#custom-range-start");
this._endRange = this.querySelector("#custom-range-end");
this._rangeInput = this.querySelector("#custom-range");
this._rangeInput.title = "";
this._rangePicker = this.querySelector("#range-picker");
this._rangeError = this.querySelector("#error-invalid-range");
this._startRangeOverflowError = this.querySelector(
"#error-invalid-start-range-overflow"
);
this._updatePageRangeTask = createDeferredTask(
() => this.updatePageRange(),
INPUT_DELAY_MS
);
this._pagesSet = new Set();
this.addEventListener("input", this);
this.addEventListener("keypress", this);
@ -1772,139 +1873,237 @@ class PageRangeInput extends PrintUIControlMixin(HTMLElement) {
}
updatePageRange() {
this.dispatchSettingsChange({
pageRanges: this._rangePicker.value
? [this._startRange.value, this._endRange.value]
: [],
});
let isAll = this._rangePicker.value == "all";
if (isAll) {
this._pagesSet.clear();
for (let i = 1; i <= this._numPages; i++) {
this._pagesSet.add(i);
}
if (!this._rangeInput.checkValidity()) {
this._rangeInput.setCustomValidity("");
this._rangeInput.value = "";
}
} else {
this.validateRangeInput();
}
this.dispatchEvent(new Event("revalidate", { bubbles: true }));
document.l10n.setAttributes(
this._rangeError,
"printui-error-invalid-range",
{
numPages: this._numPages,
}
);
// If it's valid, update the page range and hide the error messages.
// Otherwise, set the appropriate error message
if (this._rangeInput.validity.valid || isAll) {
window.clearTimeout(this.showErrorTimeoutId);
this._startRangeOverflowError.hidden = this._rangeError.hidden = true;
} else {
this._rangeInput.focus();
}
}
dispatchPageRange(shouldCancel = true) {
window.clearTimeout(this.showErrorTimeoutId);
if (this._rangeInput.validity.valid || this._rangePicker.value == "all") {
this.dispatchSettingsChange({
pageRanges: this.formatPageRange(),
});
} else {
if (shouldCancel) {
this.cancelSettingsChange({ pageRanges: true });
}
this.showErrorTimeoutId = window.setTimeout(() => {
this._rangeError.hidden =
this._rangeInput.validationMessage != "invalid";
this._startRangeOverflowError.hidden =
this._rangeInput.validationMessage != "startRangeOverflow";
}, INPUT_DELAY_MS);
}
}
// The platform expects pageRanges to be an array of
// ranges represented by ints.
// Ex: Printing pages 1-3 would return [1,3]
// Ex: Printing page 1 would return [1,1]
// Ex: Printing pages 1-2,4 would return [1,2,4,4]
formatPageRange() {
if (
this._pagesSet.size == 0 ||
this._rangeInput.value == "" ||
this._rangePicker.value == "all"
) {
// Show all pages.
return [];
}
let pages = Array.from(this._pagesSet).sort((a, b) => a - b);
let formattedRanges = [];
let startRange = pages[0];
let endRange = pages[0];
formattedRanges.push(startRange);
for (let i = 1; i < pages.length; i++) {
let currentPage = pages[i - 1];
let nextPage = pages[i];
if (nextPage > currentPage + 1) {
formattedRanges.push(endRange);
startRange = endRange = nextPage;
formattedRanges.push(startRange);
} else {
endRange = nextPage;
}
}
formattedRanges.push(endRange);
return formattedRanges;
}
update(settings) {
this.toggleAttribute("all-pages", !settings.pageRanges.length);
let { pageRanges, printerName } = settings;
this.toggleAttribute("all-pages", !pageRanges.length);
if (!this.printerName) {
this.printerName = printerName;
}
let isValid = this._rangeInput.checkValidity();
if (this.printerName != printerName && !isValid) {
this.printerName = printerName;
this._rangeInput.value = "";
this.updatePageRange();
this.dispatchPageRange();
}
}
handleKeypress(e) {
let char = String.fromCharCode(e.charCode);
let acceptedChar = char.match(/^[0-9,-]$/);
if (!acceptedChar && !char.match("\x00") && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
}
}
handlePaste(e) {
let paste = (e.clipboardData || window.clipboardData)
.getData("text")
.trim();
if (!paste.match(/^[0-9,-]*$/)) {
e.preventDefault();
}
}
// This method has been abstracted into a helper for testing purposes
_validateRangeInput(value, numPages) {
this._pagesSet.clear();
var ranges = value.split(",");
for (let range of ranges) {
let rangeParts = range.split("-");
if (rangeParts.length > 2) {
this._rangeInput.setCustomValidity("invalid");
this._rangeInput.title = "";
this._pagesSet.clear();
return;
}
let startRange = parseInt(rangeParts[0], 10);
let endRange = parseInt(
rangeParts.length == 2 ? rangeParts[1] : rangeParts[0],
10
);
if (isNaN(startRange) && isNaN(endRange)) {
continue;
}
// If the startRange was not specified, then we infer this
// to be 1.
if (isNaN(startRange) && rangeParts[0] == "") {
startRange = 1;
}
// If the end range was not specified, then we infer this
// to be the total number of pages.
if (isNaN(endRange) && rangeParts[1] == "") {
endRange = numPages;
}
// Check the range for errors
if (endRange < startRange) {
this._rangeInput.setCustomValidity("startRangeOverflow");
this._pagesSet.clear();
return;
} else if (
startRange > numPages ||
endRange > numPages ||
startRange == 0
) {
this._rangeInput.setCustomValidity("invalid");
this._rangeInput.title = "";
this._pagesSet.clear();
return;
}
for (let i = startRange; i <= endRange; i++) {
this._pagesSet.add(i);
}
}
this._rangeInput.setCustomValidity("");
}
validateRangeInput() {
let value = this._rangePicker.value == "all" ? "" : this._rangeInput.value;
this._validateRangeInput(value, this._numPages);
}
handleEvent(e) {
if (e.type == "change") {
// We handle input events rather than change events, make sure we only
// dispatch one settings change per user change.
return;
}
if (e.type == "keypress") {
if (e.target == this._startRange || e.target == this._endRange) {
if (e.target == this._rangeInput) {
this.handleKeypress(e);
}
return;
}
if (
e.type === "paste" &&
(e.target == this._startRange || e.target == this._endRange)
) {
if (e.type === "paste" && e.target == this._rangeInput) {
this.handlePaste(e);
}
this._updatePageRangeTask.disarm();
if (e.type == "page-count") {
let { totalPages } = e.detail;
this._startRange.max = this._endRange.max = this._totalPages = totalPages;
this._startRange.disabled = this._endRange.disabled = false;
let isChanged = false;
// Changing certain settings (like orientation, scale or printer) can
// change the number of pages. We need to update the start and end rages
// if their values are no longer valid.
if (!this._startRange.checkValidity()) {
this._startRange.value = this._totalPages;
isChanged = true;
// This means we have already handled the page count event
// and do not need to dispatch another event.
if (this._numPages == totalPages) {
return;
}
if (!this._endRange.checkValidity()) {
this._endRange.value = this._totalPages;
isChanged = true;
}
if (isChanged) {
window.clearTimeout(this.showErrorTimeoutId);
this._startRange.max = Math.min(this._endRange.value, totalPages);
this._endRange.min = Math.max(this._startRange.value, 1);
this.dispatchEvent(new Event("revalidate", { bubbles: true }));
this._numPages = totalPages;
this._rangeInput.disabled = false;
this.updatePageRange();
this.dispatchPageRange(false);
if (this._startRange.validity.valid && this._endRange.validity.valid) {
this.dispatchSettingsChange({
pageRanges: [this._startRange.value, this._endRange.value],
});
this._rangeError.hidden = true;
this._startRangeOverflowError.hidden = true;
}
}
return;
}
if (e.target == this._rangePicker) {
let printAll = e.target.value == "all";
this._startRange.required = this._endRange.required = !printAll;
this.querySelector(".range-group").hidden = printAll;
this._startRange.value = 1;
this._endRange.value = this._totalPages || 1;
this._rangeInput.hidden = e.target.value == "all";
this.updatePageRange();
window.clearTimeout(this.showErrorTimeoutId);
this._rangeError.hidden = true;
this._startRangeOverflowError.hidden = true;
return;
}
if (e.target == this._startRange || e.target == this._endRange) {
if (this._startRange.checkValidity()) {
this._endRange.min = this._startRange.value;
this.dispatchPageRange();
} else if (e.target == this._rangeInput) {
this._rangeInput.focus();
if (this._numPages) {
this.updatePageRange();
this.dispatchPageRange();
}
if (this._endRange.checkValidity()) {
this._startRange.max = this._endRange.value;
}
if (this._startRange.checkValidity() && this._endRange.checkValidity()) {
if (this._startRange.value && this._endRange.value) {
// Update the page range after a short delay so we don't update
// multiple times as the user types a multi-digit number or uses
// up/down/mouse wheel.
this._updatePageRangeTask.arm();
}
}
}
document.l10n.setAttributes(
this._rangeError,
"printui-error-invalid-range",
{
numPages: this._totalPages,
}
);
window.clearTimeout(this.showErrorTimeoutId);
let hasShownOverflowError = false;
let startValidity = this._startRange.validity;
let endValidity = this._endRange.validity;
// Display the startRangeOverflowError if the start range exceeds
// the end range. This means either the start range is greater than its
// max constraint, whiich is determined by the end range, or the end range
// is less than its minimum constraint, determined by the start range.
if (
!(
(startValidity.rangeOverflow && endValidity.valid) ||
(endValidity.rangeUnderflow && startValidity.valid)
)
) {
this._startRangeOverflowError.hidden = true;
} else {
hasShownOverflowError = true;
this.showErrorTimeoutId = window.setTimeout(() => {
this._startRangeOverflowError.hidden = false;
}, INPUT_DELAY_MS);
}
// Display the generic error if the startRangeOverflowError is not already
// showing and a range input is invalid.
if (hasShownOverflowError || (startValidity.valid && endValidity.valid)) {
this._rangeError.hidden = true;
} else {
this.showErrorTimeoutId = window.setTimeout(() => {
this._rangeError.hidden = false;
}, INPUT_DELAY_MS);
}
}
}
@ -1921,11 +2120,6 @@ class MarginsPicker extends PrintUIControlMixin(HTMLElement) {
this._customRightMargin = this.querySelector("#custom-margin-right");
this._marginError = this.querySelector("#error-invalid-margin");
this._updateCustomMarginsTask = createDeferredTask(
() => this.updateCustomMargins(),
INPUT_DELAY_MS
);
this.addEventListener("input", this);
this.addEventListener("keypress", this);
this.addEventListener("paste", this);
@ -2016,7 +2210,7 @@ class MarginsPicker extends PrintUIControlMixin(HTMLElement) {
// The values in custom fields should be initialized to custom margin values
// and must be overriden if they are no longer valid.
this.setAllMarginValues(settings);
this.updateMaxValues();
this.dispatchEvent(new Event("revalidate", { bubbles: true }));
this._marginError.hidden = true;
}
@ -2032,6 +2226,7 @@ class MarginsPicker extends PrintUIControlMixin(HTMLElement) {
) {
window.clearTimeout(this.showErrorTimeoutId);
this.setAllMarginValues(settings);
this.updateMaxValues();
this.dispatchEvent(new Event("revalidate", { bubbles: true }));
this._marginError.hidden = true;
}
@ -2041,8 +2236,6 @@ class MarginsPicker extends PrintUIControlMixin(HTMLElement) {
}
this._marginPicker.value = settings.margins;
}
this.updateMaxValues();
}
handleEvent(e) {
@ -2061,8 +2254,6 @@ class MarginsPicker extends PrintUIControlMixin(HTMLElement) {
this.handlePaste(e);
}
this._updateCustomMarginsTask.disarm();
if (e.target == this._marginPicker) {
let customMargin = e.target.value == "custom";
this.querySelector(".margin-group").hidden = !customMargin;
@ -2094,7 +2285,7 @@ class MarginsPicker extends PrintUIControlMixin(HTMLElement) {
this._customRightMargin.validity.valid
) {
this.formatMargin(e.target);
this._updateCustomMarginsTask.arm();
this.updateCustomMargins();
} else if (e.target.validity.stepMismatch) {
// If this is the third digit after the decimal point, we should
// truncate the string.
@ -2111,6 +2302,7 @@ class MarginsPicker extends PrintUIControlMixin(HTMLElement) {
) {
this._marginError.hidden = true;
} else {
this.cancelSettingsChange({ customMargins: true, margins: true });
this.showErrorTimeoutId = window.setTimeout(() => {
this._marginError.hidden = false;
}, INPUT_DELAY_MS);

Просмотреть файл

@ -33,17 +33,19 @@ function changeCustomToNone(helper) {
}
function assertPendingMarginsUpdate(helper) {
let marginsPicker = helper.get("margins-select");
ok(
marginsPicker._updateCustomMarginsTask.isArmed,
Object.keys(helper.win.PrintEventHandler._delayedChanges).length,
"At least one delayed task is added"
);
ok(
helper.win.PrintEventHandler._delayedSettingsChangeTask.isArmed,
"The update task is armed"
);
}
function assertNoPendingMarginsUpdate(helper) {
let marginsPicker = helper.get("margins-select");
ok(
!marginsPicker._updateCustomMarginsTask.isArmed,
!helper.win.PrintEventHandler._delayedSettingsChangeTask.isArmed,
"The update task isn't armed"
);
}
@ -240,7 +242,6 @@ add_task(async function testInvalidMarginsReset() {
"0.50",
"Left margin placeholder is correct"
);
assertNoPendingMarginsUpdate(helper);
await BrowserTestUtils.waitForCondition(
() => marginError.hidden,
"Wait for margin error to be hidden"
@ -656,7 +657,7 @@ add_task(async function testInvalidMarginResetAfterDestinationChange() {
is(destinationPicker.disabled, false, "Destination picker is enabled");
await helper.dispatchSettingsChange({ printerName: mockPrinterName });
helper.dispatchSettingsChange({ printerName: mockPrinterName });
await BrowserTestUtils.waitForCondition(
() => marginError.hidden,
"Wait for margin error to be hidden"
@ -742,7 +743,7 @@ add_task(async function testRevalidateCustomMarginsAfterOrientationChanges() {
{ marginTop: 5, marginRight: 0.5, marginBottom: 5, marginLeft: 0.5 },
{ marginTop: 0.5, marginRight: 0.5, marginBottom: 0.5, marginLeft: 0.5 },
async () => {
await helper.dispatchSettingsChange({ orientation: 1 });
helper.dispatchSettingsChange({ orientation: 1 });
await helper.waitForSettingsEvent();
ok(marginError.hidden, "Margin error is hidden");
}

Просмотреть файл

@ -12,6 +12,15 @@ function changeAllToCustom(helper) {
EventUtils.sendKey("return", helper.win);
}
function changeCustomToAll(helper) {
let rangeSelect = helper.get("range-picker");
rangeSelect.focus();
rangeSelect.scrollIntoView({ block: "center" });
EventUtils.sendKey("space", helper.win);
EventUtils.sendKey("up", helper.win);
EventUtils.sendKey("return", helper.win);
}
add_task(async function testRangeResetAfterScale() {
const mockPrinterName = "Fake Printer";
await PrintHelper.withTestPage(async helper => {
@ -20,7 +29,7 @@ add_task(async function testRangeResetAfterScale() {
await helper.setupMockPrint();
helper.mockFilePicker("changeRangeFromScale.pdf");
this.changeAllToCustom(helper);
changeAllToCustom(helper);
await helper.openMoreSettings();
let scaleRadio = helper.get("percent-scale-choice");
@ -28,20 +37,58 @@ add_task(async function testRangeResetAfterScale() {
let percentScale = helper.get("percent-scale");
await helper.waitForPreview(() => helper.text(percentScale, "200"));
let customRange = helper.get("custom-range");
let rangeError = helper.get("error-invalid-range");
await helper.waitForPreview(() => {
helper.text(helper.get("custom-range-start"), "3");
helper.text(helper.get("custom-range-end"), "3");
helper.text(customRange, "3");
});
await helper.withClosingFn(async () => {
await helper.text(percentScale, "10");
EventUtils.sendKey("return", helper.win);
helper.resolvePrint();
});
helper.assertPrintedWithSettings({
pageRanges: [1, 1],
scaling: 0.1,
ok(rangeError.hidden, "Range error is hidden");
await helper.text(percentScale, "10");
EventUtils.sendKey("return", helper.win);
await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
ok(!rangeError.hidden, "Range error is showing");
await helper.closeDialog();
});
});
add_task(async function testRangeResetAfterPaperSize() {
await PrintHelper.withTestPage(async helper => {
await helper.startPrint();
await helper.waitForPreview(() =>
helper.dispatchSettingsChange({ paperId: "iso_a5" })
);
await helper.setupMockPrint();
await helper.openMoreSettings();
let scaleRadio = helper.get("percent-scale-choice");
await helper.waitForPreview(() => helper.click(scaleRadio));
let percentScale = helper.get("percent-scale");
await helper.waitForPreview(() => helper.text(percentScale, "200"));
let customRange = helper.get("custom-range");
changeAllToCustom(helper);
await BrowserTestUtils.waitForAttributeRemoval("hidden", customRange);
let rangeError = helper.get("error-invalid-range");
await helper.waitForPreview(() => {
helper.text(customRange, "6");
});
ok(rangeError.hidden, "Range error is hidden");
helper.dispatchSettingsChange({ paperId: "iso_a3" });
await BrowserTestUtils.waitForCondition(
() => helper.get("paper-size-picker").value == "iso_a3",
"Wait for paper size select to update"
);
EventUtils.sendKey("return", helper.win);
await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
ok(!rangeError.hidden, "Range error is showing");
await helper.closeDialog();
});
});
@ -52,21 +99,16 @@ add_task(async function testInvalidRangeResetAfterDestinationChange() {
await helper.startPrint();
let destinationPicker = helper.get("printer-picker");
let startPageRange = helper.get("custom-range-start");
let customPageRange = helper.get("custom-range");
await helper.assertSettingsChanged(
{ pageRanges: [] },
{ pageRanges: [1, 1] },
async () => {
await helper.waitForPreview(() => changeAllToCustom(helper));
}
);
await helper.assertSettingsNotChanged({ pageRanges: [] }, async () => {
changeAllToCustom(helper);
});
let rangeError = helper.get("error-invalid-range");
let rangeError = helper.get("error-invalid-start-range-overflow");
await helper.assertSettingsNotChanged({ pageRanges: [1, 1] }, async () => {
await helper.assertSettingsNotChanged({ pageRanges: [] }, async () => {
ok(rangeError.hidden, "Range error is hidden");
await helper.text(startPageRange, "9");
await helper.text(customPageRange, "9");
await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
ok(!rangeError.hidden, "Range error is showing");
});
@ -74,13 +116,164 @@ add_task(async function testInvalidRangeResetAfterDestinationChange() {
is(destinationPicker.disabled, false, "Destination picker is enabled");
// Select a new printer
await helper.dispatchSettingsChange({ printerName: mockPrinterName });
helper.dispatchSettingsChange({ printerName: mockPrinterName });
await BrowserTestUtils.waitForCondition(
() => rangeError.hidden,
"Wait for range error to be hidden"
);
is(startPageRange.value, "1", "Start page range has reset to 1");
is(customPageRange.value, "", "Page range has reset");
await helper.closeDialog();
});
});
add_task(async function testPageRangeSets() {
await PrintHelper.withTestPage(async helper => {
await helper.startPrint();
let customRange = helper.get("custom-range");
let pageRangeInput = helper.get("page-range-input");
let invalidError = helper.get("error-invalid-range");
let invalidOverflowError = helper.get("error-invalid-start-range-overflow");
ok(customRange.hidden, "Custom range input is hidden");
changeAllToCustom(helper);
await BrowserTestUtils.waitForAttributeRemoval("hidden", customRange);
ok(!customRange.hidden, "Custom range is showing");
// We need to set the input to something to ensure we do not return early
// out of our validation function
helper.text(helper.get("custom-range"), ",");
let validStrings = {
"1": [1, 1],
"1,": [1, 1],
"2": [2, 2],
"1-2": [1, 2],
"1,2": [1, 2],
"1,2,": [1, 2],
"2,1": [1, 2],
"1,3": [1, 1, 3, 3],
"1-1,3": [1, 1, 3, 3],
"1,3-3": [1, 1, 3, 3],
"10-33": [10, 33],
"1-": [1, 50],
"-": [],
"-20": [1, 20],
"-,1": [],
"-1,1-": [],
"-1,1-2": [1, 2],
",9": [9, 9],
",": [],
"1,2,1,20,5": [1, 2, 5, 5, 20, 20],
"1-17,4,12-19": [1, 19],
"43-46,42,47-": [42, 50],
};
for (let [str, expected] of Object.entries(validStrings)) {
pageRangeInput._validateRangeInput(str, 50);
let pageRanges = pageRangeInput.formatPageRange();
is(
expected.every((page, index) => page === pageRanges[index]),
true,
`Expected page range for "${str}" matches "${expected}"`
);
ok(invalidError.hidden, "Generic error message is hidden");
ok(invalidOverflowError.hidden, "Start overflow error message is hidden");
}
let invalidStrings = ["51", "1,51", "1-51", "4-1", "--", "0", "-90"];
for (let str of invalidStrings) {
pageRangeInput._validateRangeInput(str, 50);
is(pageRangeInput._pagesSet.size, 0, `There are no pages in the set`);
ok(!pageRangeInput.validity, "Input is invalid");
}
await helper.closeDialog();
});
});
add_task(async function testRangeError() {
await PrintHelper.withTestPage(async helper => {
await helper.startPrint();
changeAllToCustom(helper);
let invalidError = helper.get("error-invalid-range");
let invalidOverflowError = helper.get("error-invalid-start-range-overflow");
ok(invalidError.hidden, "Generic error message is hidden");
ok(invalidOverflowError.hidden, "Start overflow error message is hidden");
helper.text(helper.get("custom-range"), "4");
await BrowserTestUtils.waitForAttributeRemoval("hidden", invalidError);
ok(!invalidError.hidden, "Generic error message is showing");
ok(invalidOverflowError.hidden, "Start overflow error message is hidden");
await helper.closeDialog();
});
});
add_task(async function testStartOverflowRangeError() {
await PrintHelper.withTestPage(async helper => {
await helper.startPrint();
changeAllToCustom(helper);
await helper.openMoreSettings();
let scaleRadio = helper.get("percent-scale-choice");
await helper.waitForPreview(() => helper.click(scaleRadio));
let percentScale = helper.get("percent-scale");
await helper.waitForPreview(() => helper.text(percentScale, "200"));
let invalidError = helper.get("error-invalid-range");
let invalidOverflowError = helper.get("error-invalid-start-range-overflow");
ok(invalidError.hidden, "Generic error message is hidden");
ok(invalidOverflowError.hidden, "Start overflow error message is hidden");
helper.text(helper.get("custom-range"), "2-1");
await BrowserTestUtils.waitForAttributeRemoval(
"hidden",
invalidOverflowError
);
ok(invalidError.hidden, "Generic error message is hidden");
ok(!invalidOverflowError.hidden, "Start overflow error message is showing");
await helper.closeDialog();
});
});
add_task(async function testErrorClearedAfterSwitchingToAll() {
await PrintHelper.withTestPage(async helper => {
await helper.startPrint();
changeAllToCustom(helper);
let customRange = helper.get("custom-range");
let rangeError = helper.get("error-invalid-range");
ok(rangeError.hidden, "Generic error message is hidden");
helper.text(customRange, "3");
await BrowserTestUtils.waitForAttributeRemoval("hidden", rangeError);
ok(!rangeError.hidden, "Generic error message is showing");
changeCustomToAll(helper);
await BrowserTestUtils.waitForCondition(
() => rangeError.hidden,
"Wait for range error to be hidden"
);
ok(customRange.hidden, "Custom range is hidden");
await helper.closeDialog();
});
});

Просмотреть файл

@ -103,6 +103,10 @@ add_task(async function testSheetCountPageRange() {
);
let sheetCount = helper.get("sheet-count");
await BrowserTestUtils.waitForCondition(
() => getSheetCount(sheetCount) != 1,
"Wait for sheet count to update"
);
let sheets = getSheetCount(sheetCount);
ok(sheets >= 3, "There are at least 3 pages");
@ -140,6 +144,10 @@ add_task(async function testPagesPerSheetCount() {
);
let sheetCount = helper.get("sheet-count");
await BrowserTestUtils.waitForCondition(
() => getSheetCount(sheetCount) != 1,
"Wait for sheet count to update"
);
let sheets = getSheetCount(sheetCount);
ok(sheets > 1, "There are multiple pages");

Просмотреть файл

@ -268,6 +268,10 @@ class PrintHelper {
async waitForSettingsEvent(changeFn) {
let changed = BrowserTestUtils.waitForEvent(this.doc, "print-settings");
await changeFn?.();
await BrowserTestUtils.waitForCondition(
() => !this.win.PrintEventHandler._delayedSettingsChangeTask.isArmed,
"Wait for all delayed tasks to execute"
);
await changed;
}

Просмотреть файл

@ -19,14 +19,9 @@ printui-page-range-custom = Custom
printui-page-range-label = Pages
printui-page-range-picker =
.aria-label = Pick page range
printui-page-custom-range =
printui-page-custom-range-input =
.aria-label = Enter custom page range
# This label is displayed before the first input field indicating
# the start of the range to print.
printui-range-start = From
# This label is displayed between the input fields indicating
# the start and end page of the range to print.
printui-range-end = to
.placeholder = e.g. 2-6, 9, 12-16
# Section title for the number of copies to print
printui-copies-label = Copies