| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| cr.exportPath('print_preview_new'); |
| |
| /** |
| * @const {!Map<!print_preview.ticket_items.CustomMarginsOrientation, string>} |
| */ |
| print_preview_new.MARGIN_KEY_MAP = new Map([ |
| [print_preview.ticket_items.CustomMarginsOrientation.TOP, 'marginTop'], |
| [print_preview.ticket_items.CustomMarginsOrientation.RIGHT, 'marginRight'], |
| [print_preview.ticket_items.CustomMarginsOrientation.BOTTOM, 'marginBottom'], |
| [print_preview.ticket_items.CustomMarginsOrientation.LEFT, 'marginLeft'] |
| ]); |
| |
| (function() { |
| 'use strict'; |
| |
| /** @const {number} */ |
| const MINIMUM_DISTANCE = 72; // 1 inch |
| |
| Polymer({ |
| is: 'print-preview-margin-control-container', |
| |
| behaviors: [SettingsBehavior], |
| |
| properties: { |
| /** @type {!print_preview.Size} */ |
| pageSize: { |
| type: Object, |
| notify: true, |
| }, |
| |
| /** @type {!print_preview.Margins} */ |
| documentMargins: { |
| type: Object, |
| notify: true, |
| }, |
| |
| previewLoaded: Boolean, |
| |
| /** @type {?print_preview.MeasurementSystem} */ |
| measurementSystem: Object, |
| |
| /** @type {!print_preview_new.State} */ |
| state: { |
| type: Number, |
| observer: 'onStateChanged_', |
| }, |
| |
| /** @private {number} */ |
| scaleTransform_: { |
| type: Number, |
| notify: true, |
| value: 0, |
| }, |
| |
| /** @private {!print_preview.Coordinate2d} */ |
| translateTransform_: { |
| type: Object, |
| notify: true, |
| value: new print_preview.Coordinate2d(0, 0), |
| }, |
| |
| /** @private {?print_preview.Size} */ |
| clipSize_: { |
| type: Object, |
| notify: true, |
| value: null, |
| }, |
| |
| /** @private {boolean} */ |
| available_: { |
| type: Boolean, |
| notify: true, |
| computed: 'computeAvailable_(previewLoaded, settings.margins.value)', |
| observer: 'onAvailableChange_', |
| }, |
| |
| /** @private {boolean} */ |
| invisible_: { |
| type: Boolean, |
| reflectToAttribute: true, |
| value: true, |
| }, |
| |
| /** |
| * @private {!Array<!print_preview.ticket_items.CustomMarginsOrientation>} |
| */ |
| marginSides_: { |
| type: Array, |
| notify: true, |
| value: [ |
| print_preview.ticket_items.CustomMarginsOrientation.TOP, |
| print_preview.ticket_items.CustomMarginsOrientation.RIGHT, |
| print_preview.ticket_items.CustomMarginsOrientation.BOTTOM, |
| print_preview.ticket_items.CustomMarginsOrientation.LEFT, |
| ], |
| }, |
| |
| /** |
| * String attribute used to set cursor appearance. Possible values: |
| * empty (''): No margin control is currently being dragged. |
| * 'dragging-horizontal': The left or right control is being dragged. |
| * 'dragging-vertical': The top or bottom control is being dragged. |
| * @private {string} |
| */ |
| dragging_: { |
| type: String, |
| reflectToAttribute: true, |
| value: '', |
| }, |
| }, |
| |
| /** @private {boolean} */ |
| textboxFocused_: false, |
| |
| observers: [ |
| 'onMarginSettingsChange_(settings.customMargins.value)', |
| 'onMediaSizeOrLayoutChange_(' + |
| 'settings.mediaSize.value, settings.layout.value)', |
| ], |
| |
| /** @private {!print_preview.Coordinate2d} */ |
| pointerStartPositionInPixels_: new print_preview.Coordinate2d(0, 0), |
| |
| /** @private {?print_preview.Coordinate2d} */ |
| marginStartPositionInPixels_: null, |
| |
| /** @private {?boolean} */ |
| resetMargins_: null, |
| |
| /** |
| * @return {boolean} |
| * @private |
| */ |
| computeAvailable_: function() { |
| return this.previewLoaded && !!this.clipSize_ && |
| this.getSettingValue('margins') == |
| print_preview.ticket_items.MarginsTypeValue.CUSTOM && |
| !!this.pageSize; |
| }, |
| |
| /** @private */ |
| onAvailableChange_: function() { |
| if (this.available_ && this.resetMargins_) { |
| // Set the custom margins values to the current document margins if the |
| // custom margins were reset. |
| const newMargins = {}; |
| for (const side of Object.values( |
| print_preview.ticket_items.CustomMarginsOrientation)) { |
| const key = print_preview_new.MARGIN_KEY_MAP.get(side); |
| newMargins[key] = this.documentMargins.get(side); |
| } |
| this.setSetting('customMargins', newMargins); |
| this.resetMargins_ = false; |
| } |
| this.invisible_ = !this.available_; |
| }, |
| |
| /** @private */ |
| onMarginSettingsChange_: function() { |
| const margins = this.getSettingValue('customMargins'); |
| this.shadowRoot.querySelectorAll('print-preview-margin-control') |
| .forEach(control => { |
| const key = print_preview_new.MARGIN_KEY_MAP.get(control.side); |
| const newValue = margins[key] || 0; |
| control.setPositionInPts(newValue); |
| control.setTextboxValue(this.serializeValueFromPts_(newValue)); |
| }); |
| }, |
| |
| /** @private */ |
| onMediaSizeOrLayoutChange_: function() { |
| // Reset the custom margins when the paper size changes. Don't do this if it |
| // is the first preview. |
| if (this.resetMargins_ === null) |
| return; |
| |
| this.resetMargins_ = true; |
| const marginsSetting = this.getSetting('margins'); |
| if (marginsSetting.value == |
| print_preview.ticket_items.MarginsTypeValue.CUSTOM) { |
| // Set the margins value to default first. |
| this.setSetting( |
| 'margins', print_preview.ticket_items.MarginsTypeValue.DEFAULT); |
| } |
| // Reset custom margins so that the sticky value is not restored for the new |
| // paper size. |
| this.setSetting('customMargins', {}); |
| }, |
| |
| /** @private */ |
| onStateChanged_: function() { |
| if (this.state == print_preview_new.State.READY && |
| this.resetMargins_ === null) { |
| // Don't reset margins if there are sticky values. Otherwise, set them to |
| // the document margins when the user selects custom margins. |
| this.resetMargins_ = |
| this.getSettingValue('customMargins').marginTop === undefined; |
| } |
| }, |
| |
| /** |
| * @param {!print_preview.ticket_items.CustomMarginsOrientation} orientation |
| * Orientation value to test. |
| * @return {boolean} Whether the given orientation is TOP or BOTTOM. |
| * @private |
| */ |
| isTopOrBottom_: function(orientation) { |
| return orientation == |
| print_preview.ticket_items.CustomMarginsOrientation.TOP || |
| orientation == |
| print_preview.ticket_items.CustomMarginsOrientation.BOTTOM; |
| }, |
| |
| /** |
| * @param {!HTMLElement} control Control being repositioned. |
| * @param {!print_preview.Coordinate2d} posInPixels Desired position, in |
| * pixels. |
| * @return {number} The new position for the control, in pts. Returns the |
| * position for the dimension that the control operates in, i.e. |
| * x direction for the left/right controls, y direction otherwise. |
| * @private |
| */ |
| posInPixelsToPts_: function(control, posInPixels) { |
| const side = |
| /** @type {print_preview.ticket_items.CustomMarginsOrientation} */ ( |
| control.side); |
| return this.clipAndRoundValue_( |
| side, |
| control.convertPixelsToPts( |
| this.isTopOrBottom_(side) ? posInPixels.y : posInPixels.x)); |
| }, |
| |
| /** |
| * Moves the position of the given control to the desired position in pts |
| * within some constraint minimum and maximum. |
| * @param {!HTMLElement} control Control to move. |
| * @param {number} posInPts Desired position to move to, in pts. Position is |
| * 1 dimensional and represents position in the x direction if control is |
| * for the left or right margin, and the y direction otherwise. |
| * @private |
| */ |
| moveControlWithConstraints_: function(control, posInPts) { |
| control.setPositionInPts(posInPts); |
| control.setTextboxValue(this.serializeValueFromPts_(posInPts)); |
| }, |
| |
| /** |
| * @param {number} value Value in points to serialize. |
| * @return {string} String representation of the value in the system's local |
| * units. |
| * @private |
| */ |
| serializeValueFromPts_: function(value) { |
| assert(this.measurementSystem); |
| value = this.measurementSystem.convertFromPoints(value); |
| value = this.measurementSystem.roundValue(value); |
| return value + this.measurementSystem.unitSymbol; |
| }, |
| |
| /** |
| * Translates the position of the margin control relative to the pointer |
| * position in pixels. |
| * @param {!print_preview.Coordinate2d} pointerPosition New position of |
| * the pointer. |
| * @return {!print_preview.Coordinate2d} New position of the margin control. |
| */ |
| translatePointerToPositionInPixels: function(pointerPosition) { |
| return new print_preview.Coordinate2d( |
| pointerPosition.x - this.pointerStartPositionInPixels_.x + |
| this.marginStartPositionInPixels_.x, |
| pointerPosition.y - this.pointerStartPositionInPixels_.y + |
| this.marginStartPositionInPixels_.y); |
| }, |
| |
| /** |
| * Called when the pointer moves in the custom margins component. Moves the |
| * dragged margin control. |
| * @param {!PointerEvent} event Contains the position of the pointer. |
| * @private |
| */ |
| onPointerMove_: function(event) { |
| const control = |
| /** @type {!PrintPreviewMarginControlElement} */ (event.target); |
| const posInPts = this.posInPixelsToPts_( |
| control, |
| this.translatePointerToPositionInPixels( |
| new print_preview.Coordinate2d(event.x, event.y))); |
| this.moveControlWithConstraints_(control, posInPts); |
| }, |
| |
| /** |
| * Called when the pointer is released in the custom margins component. |
| * Releases the dragged margin control. |
| * @param {!PointerEvent} event Contains the position of the pointer. |
| * @private |
| */ |
| onPointerUp_: function(event) { |
| const control = |
| /** @type {!PrintPreviewMarginControlElement} */ (event.target); |
| this.dragging_ = ''; |
| const posInPixels = this.translatePointerToPositionInPixels( |
| new print_preview.Coordinate2d(event.x, event.y)); |
| const posInPts = this.posInPixelsToPts_(control, posInPixels); |
| this.moveControlWithConstraints_(control, posInPts); |
| this.setMargin_(control.side, posInPts); |
| this.updateClippingMask(this.clipSize_); |
| this.unlisten(control, 'pointercancel', 'onPointerUp_'); |
| this.unlisten(control, 'pointerup', 'onPointerUp_'); |
| this.unlisten(control, 'pointermove', 'onPointerMove_'); |
| |
| this.fire('margin-drag-changed', false); |
| }, |
| |
| /** |
| * @param {boolean} invisible Whether the margin controls should be |
| * invisible. |
| */ |
| setInvisible: function(invisible) { |
| // Ignore changes if the margin controls are not available. |
| if (!this.available_) |
| return; |
| |
| // Do not set the controls invisible if the user is dragging or focusing |
| // the textbox for one of them. |
| if (invisible && (this.dragging_ != '' || this.textboxFocused_)) |
| return; |
| |
| this.invisible_ = invisible; |
| }, |
| |
| /** |
| * @param {!CustomEvent} e Contains information about what control fired the |
| * event. |
| * @private |
| */ |
| onTextFocus_: function(e) { |
| this.textboxFocused_ = true; |
| const control = /** @type {!PrintPreviewMarginControlElement} */ (e.target); |
| |
| const x = control.offsetLeft; |
| const y = control.offsetTop; |
| const isTopOrBottom = this.isTopOrBottom_( |
| /** @type {!print_preview.ticket_items.CustomMarginsOrientation} */ ( |
| control.side)); |
| const position = {}; |
| // Extra padding, in px, to ensure the full textbox will be visible and not |
| // just a portion of it. Can't be less than half the width or height of the |
| // clip area for the computations below to work. |
| const padding = Math.min( |
| Math.min(this.clipSize_.width / 2, this.clipSize_.height / 2), 50); |
| |
| // Note: clipSize_ gives the current visible area of the margin control |
| // container. The offsets of the controls are relative to the origin of this |
| // visible area. |
| if (isTopOrBottom) { |
| // For top and bottom controls, the horizontal position of the box is |
| // around halfway across the control's width. |
| position.x = Math.min(x + control.offsetWidth / 2 - padding, 0); |
| position.x = Math.max( |
| x + control.offsetWidth / 2 + padding - this.clipSize_.width, |
| position.x); |
| // For top and bottom controls, the vertical position of the box is nearly |
| // the same as the vertical position of the control. |
| position.y = Math.min(y - padding, 0); |
| position.y = Math.max(y - this.clipSize_.height + padding, position.y); |
| } else { |
| // For left and right controls, the horizontal position of the box is |
| // nearly the same as the horizontal position of the control. |
| position.x = Math.min(x - padding, 0); |
| position.x = Math.max(x - this.clipSize_.width + padding, position.x); |
| // For top and bottom controls, the vertical position of the box is |
| // around halfway up the control's height. |
| position.y = Math.min(y + control.offsetHeight / 2 - padding, 0); |
| position.y = Math.max( |
| y + control.offsetHeight / 2 + padding - this.clipSize_.height, |
| position.y); |
| } |
| |
| this.fire('text-focus-position', position); |
| }, |
| |
| /** |
| * @param {string} side The margin side. Must be a CustomMarginsOrientation. |
| * @param {number} marginValue New value for the margin in points. |
| * @private |
| */ |
| setMargin_: function(side, marginValue) { |
| const marginSide = |
| /** @type {!print_preview.ticket_items.CustomMarginsOrientation} */ ( |
| side); |
| const oldMargins = /** @type {print_preview.MarginsSetting} */ ( |
| this.getSettingValue('customMargins')); |
| const key = print_preview_new.MARGIN_KEY_MAP.get(marginSide); |
| if (oldMargins[key] == marginValue) |
| return; |
| const newMargins = Object.assign({}, oldMargins); |
| newMargins[key] = marginValue; |
| this.setSetting('customMargins', newMargins); |
| }, |
| |
| /** |
| * @param {string} side The margin side. Must be a CustomMarginsOrientation. |
| * @param {number} value The new margin value in points. |
| * @return {number} The clipped margin value in points. |
| * @private |
| */ |
| clipAndRoundValue_: function(side, value) { |
| const marginSide = |
| /** @type {!print_preview.ticket_items.CustomMarginsOrientation} */ ( |
| side); |
| if (value < 0) |
| return 0; |
| const Orientation = print_preview.ticket_items.CustomMarginsOrientation; |
| let limit = 0; |
| const margins = this.getSettingValue('customMargins'); |
| if (marginSide == Orientation.TOP) |
| limit = this.pageSize.height - margins.marginBottom - MINIMUM_DISTANCE; |
| else if (marginSide == Orientation.RIGHT) |
| limit = this.pageSize.width - margins.marginLeft - MINIMUM_DISTANCE; |
| else if (marginSide == Orientation.BOTTOM) |
| limit = this.pageSize.height - margins.marginTop - MINIMUM_DISTANCE; |
| else { |
| assert(marginSide == Orientation.LEFT); |
| limit = this.pageSize.width - margins.marginRight - MINIMUM_DISTANCE; |
| } |
| return Math.round(Math.min(value, limit)); |
| }, |
| |
| /** |
| * @param {!CustomEvent} e Event containing the new textbox value. |
| * @private |
| */ |
| onTextChange_: function(e) { |
| const marginValue = this.parseValueToPts_(/** @type {string} */ (e.detail)); |
| const control = |
| /** @type {!PrintPreviewMarginControlElement} */ (e.target); |
| if (marginValue == null) { |
| control.invalid = true; |
| return; |
| } |
| control.invalid = false; |
| const clippedValue = this.clipAndRoundValue_(control.side, marginValue); |
| control.setPositionInPts(clippedValue); |
| this.setMargin_(control.side, clippedValue); |
| }, |
| |
| /** |
| * @param {!CustomEvent} e Event fired when a control's text field is blurred. |
| * Contains information about whether the control is in an invalid state. |
| * @private |
| */ |
| onTextBlur_: function(e) { |
| if (e.detail /* detail is true if the control is in an invalid state */) { |
| const control = |
| /** @type {!PrintPreviewMarginControlElement} */ (e.target); |
| control.setTextboxValue( |
| this.serializeValueFromPts_(control.getPositionInPts())); |
| control.invalid = false; |
| } |
| this.textboxFocused_ = false; |
| }, |
| |
| /** |
| * @param {!PointerEvent} e Fired when pointerdown occurs on a margin control. |
| * @private |
| */ |
| onPointerDown_: function(e) { |
| const control = |
| /** @type {!PrintPreviewMarginControlElement} */ (e.target); |
| if (!control.shouldDrag(e)) |
| return; |
| |
| this.pointerStartPositionInPixels_ = |
| new print_preview.Coordinate2d(e.x, e.y); |
| this.marginStartPositionInPixels_ = |
| new print_preview.Coordinate2d(control.offsetLeft, control.offsetTop); |
| this.dragging_ = |
| this.isTopOrBottom_( |
| /** @type {print_preview.ticket_items.CustomMarginsOrientation} */ ( |
| control.side)) ? |
| 'dragging-vertical' : |
| 'dragging-horizontal'; |
| this.listen(control, 'pointercancel', 'onPointerUp_'); |
| this.listen(control, 'pointerup', 'onPointerUp_'); |
| this.listen(control, 'pointermove', 'onPointerMove_'); |
| control.setPointerCapture(e.pointerId); |
| |
| this.fire('margin-drag-changed', true); |
| }, |
| |
| /** |
| * Set display:none after the opacity transition for the controls is done. |
| * @private |
| */ |
| onTransitionEnd_: function() { |
| if (this.invisible_) |
| this.style.display = 'none'; |
| }, |
| |
| /** |
| * @param {string} value Value to parse to points. E.g. '3.40"' or '200mm'. |
| * @return {?number} Value in points represented by the input value. |
| * @private |
| */ |
| parseValueToPts_: function(value) { |
| // Removing whitespace anywhere in the string. |
| value = value.replace(/\s*/g, ''); |
| if (value.length == 0) { |
| return null; |
| } |
| assert(this.measurementSystem); |
| const validationRegex = new RegExp( |
| '^(^-?)(\\d)+(\\' + this.measurementSystem.thousandsDelimeter + |
| '\\d{3})*(\\' + this.measurementSystem.decimalDelimeter + '\\d*)?' + |
| '(' + this.measurementSystem.unitSymbol + ')?$'); |
| if (validationRegex.test(value)) { |
| // Replacing decimal point with the dot symbol in order to use |
| // parseFloat() properly. |
| const replacementRegex = |
| new RegExp('\\' + this.measurementSystem.decimalDelimeter + '{1}'); |
| value = value.replace(replacementRegex, '.'); |
| return this.measurementSystem.convertToPoints(parseFloat(value)); |
| } |
| return null; |
| }, |
| |
| /** |
| * Updates the translation transformation that translates pixel values in |
| * the space of the HTML DOM. |
| * @param {print_preview.Coordinate2d} translateTransform Updated value of |
| * the translation transformation. |
| */ |
| updateTranslationTransform: function(translateTransform) { |
| if (!translateTransform.equals(this.translateTransform_)) { |
| this.translateTransform_ = translateTransform; |
| } |
| }, |
| |
| /** |
| * Updates the scaling transform that scales pixels values to point values. |
| * @param {number} scaleTransform Updated value of the scale transform. |
| */ |
| updateScaleTransform: function(scaleTransform) { |
| if (scaleTransform != this.scaleTransform_) |
| this.scaleTransform_ = scaleTransform; |
| }, |
| |
| /** |
| * Clips margin controls to the given clip size in pixels. |
| * @param {print_preview.Size} clipSize Size to clip the margin controls to. |
| */ |
| updateClippingMask: function(clipSize) { |
| if (!clipSize) { |
| return; |
| } |
| this.clipSize_ = clipSize; |
| this.notifyPath('clipSize_'); |
| }, |
| }); |
| })(); |