| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import './margin_control.js'; |
| |
| import {assert} from 'chrome://resources/js/assert.js'; |
| import {EventTracker} from 'chrome://resources/js/event_tracker.js'; |
| import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js'; |
| import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js'; |
| |
| import {Coordinate2d} from '../data/coordinate2d.js'; |
| import type {Margins, MarginsSetting} from '../data/margins.js'; |
| import {CustomMarginsOrientation, MarginsType} from '../data/margins.js'; |
| import type {MeasurementSystem} from '../data/measurement_system.js'; |
| import {Size} from '../data/size.js'; |
| import {State} from '../data/state.js'; |
| |
| import type {PrintPreviewMarginControlElement} from './margin_control.js'; |
| import {getCss} from './margin_control_container.css.js'; |
| import {getHtml} from './margin_control_container.html.js'; |
| import {SettingsMixin} from './settings_mixin.js'; |
| |
| export const MARGIN_KEY_MAP: |
| Map<CustomMarginsOrientation, keyof MarginsSetting> = new Map([ |
| [CustomMarginsOrientation.TOP, 'marginTop'], |
| [CustomMarginsOrientation.RIGHT, 'marginRight'], |
| [CustomMarginsOrientation.BOTTOM, 'marginBottom'], |
| [CustomMarginsOrientation.LEFT, 'marginLeft'], |
| ]); |
| |
| const MINIMUM_DISTANCE: number = 72; // 1 inch |
| |
| |
| const PrintPreviewMarginControlContainerElementBase = |
| SettingsMixin(CrLitElement); |
| |
| export class PrintPreviewMarginControlContainerElement extends |
| PrintPreviewMarginControlContainerElementBase { |
| static get is() { |
| return 'print-preview-margin-control-container'; |
| } |
| |
| static override get styles() { |
| return getCss(); |
| } |
| |
| override render() { |
| return getHtml.bind(this)(); |
| } |
| |
| static override get properties() { |
| return { |
| pageSize: {type: Object}, |
| documentMargins: {type: Object}, |
| previewLoaded: {type: Boolean}, |
| measurementSystem: {type: Object}, |
| state: {type: Number}, |
| scaleTransform_: {type: Number}, |
| translateTransform_: {type: Object}, |
| clipSize_: {type: Object}, |
| available_: {type: Boolean}, |
| invisible_: {type: Boolean}, |
| marginSides_: {type: Array}, |
| |
| /** |
| * 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. |
| */ |
| dragging_: { |
| type: String, |
| reflect: true, |
| }, |
| }; |
| } |
| |
| accessor pageSize: Size = new Size(612, 792); |
| accessor documentMargins: Margins|null = null; |
| accessor previewLoaded: boolean = false; |
| accessor measurementSystem: MeasurementSystem|null = null; |
| accessor state: State = State.NOT_READY; |
| private accessor available_: boolean = false; |
| protected accessor invisible_: boolean = true; |
| protected accessor clipSize_: Size = new Size(0, 0); |
| protected accessor scaleTransform_: number = 0; |
| protected accessor translateTransform_: Coordinate2d = new Coordinate2d(0, 0); |
| private accessor dragging_: ''|'dragging-horizontal'|'dragging-vertical' = ''; |
| protected accessor marginSides_: CustomMarginsOrientation[] = [ |
| CustomMarginsOrientation.TOP, |
| CustomMarginsOrientation.RIGHT, |
| CustomMarginsOrientation.BOTTOM, |
| CustomMarginsOrientation.LEFT, |
| ]; |
| private pointerStartPositionInPixels_: Coordinate2d = new Coordinate2d(0, 0); |
| private marginStartPositionInPixels_: Coordinate2d|null = null; |
| private resetMargins_: boolean|null = null; |
| private eventTracker_: EventTracker = new EventTracker(); |
| private textboxFocused_: boolean = false; |
| |
| override connectedCallback() { |
| super.connectedCallback(); |
| |
| this.addSettingObserver( |
| 'customMargins.value', this.onMarginSettingsChange_.bind(this)); |
| this.onMarginSettingsChange_(); |
| |
| this.addSettingObserver( |
| 'mediaSize.value', this.onMediaSizeOrLayoutChange_.bind(this)); |
| this.addSettingObserver( |
| 'layout.value', this.onMediaSizeOrLayoutChange_.bind(this)); |
| |
| this.addSettingObserver('margins.value', () => { |
| this.available_ = this.computeAvailable_(); |
| }); |
| } |
| |
| override willUpdate(changedProperties: PropertyValues<this>) { |
| super.willUpdate(changedProperties); |
| |
| const changedPrivateProperties = |
| changedProperties as Map<PropertyKey, unknown>; |
| |
| if (changedProperties.has('state')) { |
| this.onStateChanged_(); |
| } |
| |
| if (changedProperties.has('previewLoaded')) { |
| this.available_ = this.computeAvailable_(); |
| } |
| |
| if (changedPrivateProperties.has('available_')) { |
| this.onAvailableChange_(); |
| } |
| } |
| |
| private computeAvailable_(): boolean { |
| return this.previewLoaded && |
| ((this.getSettingValue('margins') as MarginsType) === |
| MarginsType.CUSTOM) && |
| !!this.pageSize; |
| } |
| |
| private onAvailableChange_() { |
| if (this.available_ && this.resetMargins_) { |
| // Set the custom margins values to the current document margins if the |
| // custom margins were reset. |
| const newMargins: Partial<MarginsSetting> = {}; |
| assert(this.documentMargins); |
| for (const side of Object.values(CustomMarginsOrientation)) { |
| const key = MARGIN_KEY_MAP.get(side)!; |
| newMargins[key] = this.documentMargins.get(side); |
| } |
| this.setSetting('customMargins', newMargins); |
| this.resetMargins_ = false; |
| } |
| this.invisible_ = !this.available_; |
| } |
| |
| private onMarginSettingsChange_() { |
| const margins = this.getSettingValue('customMargins') as MarginsSetting; |
| if (!margins || margins.marginTop === undefined) { |
| // This may be called when print preview model initially sets the |
| // settings. It sets custom margins empty by default. |
| return; |
| } |
| this.shadowRoot.querySelectorAll('print-preview-margin-control') |
| .forEach(control => { |
| const key = MARGIN_KEY_MAP.get(control.side)!; |
| const newValue = margins[key] || 0; |
| control.setPositionInPts(newValue); |
| control.setTextboxValue(newValue); |
| }); |
| } |
| |
| private onMediaSizeOrLayoutChange_() { |
| // 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; |
| // Reset custom margins so that the sticky value is not restored for the new |
| // paper size. |
| this.setSetting('customMargins', {}); |
| } |
| |
| private onStateChanged_() { |
| if (this.state === 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. |
| const margins = this.getSettingValue('customMargins'); |
| this.resetMargins_ = !margins || margins.marginTop === undefined; |
| } |
| } |
| |
| /** |
| * @return Whether the controls should be disabled. |
| */ |
| protected controlsDisabled_(): boolean { |
| return this.state !== State.READY || this.invisible_; |
| } |
| |
| /** |
| * @param orientation Orientation value to test. |
| * @return Whether the given orientation is TOP or BOTTOM. |
| */ |
| private isTopOrBottom_(orientation: CustomMarginsOrientation): boolean { |
| return orientation === CustomMarginsOrientation.TOP || |
| orientation === CustomMarginsOrientation.BOTTOM; |
| } |
| |
| /** |
| * @param control Control being repositioned. |
| * @param posInPixels Desired position, in pixels. |
| * @return 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_( |
| control: PrintPreviewMarginControlElement, |
| posInPixels: Coordinate2d): number { |
| const side = 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 control Control to move. |
| * @param 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_( |
| control: PrintPreviewMarginControlElement, posInPts: number) { |
| control.setPositionInPts(posInPts); |
| control.setTextboxValue(posInPts); |
| } |
| |
| /** |
| * Translates the position of the margin control relative to the pointer |
| * position in pixels. |
| * @param pointerPosition New position of the pointer. |
| * @return New position of the margin control. |
| */ |
| translatePointerToPositionInPixels(pointerPosition: Coordinate2d): |
| Coordinate2d { |
| return new 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 event Contains the position of the pointer. |
| */ |
| private onPointerMove_(event: PointerEvent) { |
| const control = event.target as PrintPreviewMarginControlElement; |
| const posInPts = this.posInPixelsToPts_( |
| control, |
| this.translatePointerToPositionInPixels( |
| new 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 event Contains the position of the pointer. |
| */ |
| private onPointerUp_(event: PointerEvent) { |
| const control = event.target as PrintPreviewMarginControlElement; |
| this.dragging_ = ''; |
| const posInPixels = this.translatePointerToPositionInPixels( |
| new Coordinate2d(event.x, event.y)); |
| const posInPts = this.posInPixelsToPts_(control, posInPixels); |
| this.moveControlWithConstraints_(control, posInPts); |
| this.setMargin_(control.side, posInPts); |
| this.eventTracker_.remove(control, 'pointercancel'); |
| this.eventTracker_.remove(control, 'pointerup'); |
| this.eventTracker_.remove(control, 'pointermove'); |
| |
| this.fireDragChanged_(false); |
| } |
| |
| /** |
| * @param invisible Whether the margin controls should be invisible. |
| */ |
| setInvisible(invisible: boolean) { |
| // 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 e Contains information about what control fired the event. |
| */ |
| protected onTextFocus_(e: Event) { |
| this.textboxFocused_ = true; |
| const control = e.target as PrintPreviewMarginControlElement; |
| |
| const x = control.offsetLeft; |
| const y = control.offsetTop; |
| const isTopOrBottom = this.isTopOrBottom_(control.side); |
| const position: {x?: number, y?: number} = {}; |
| // 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.dispatchEvent(new CustomEvent( |
| 'text-focus-position', |
| {bubbles: true, composed: true, detail: position})); |
| } |
| |
| /** |
| * @param marginSide The margin side. Must be a CustomMarginsOrientation. |
| * @param marginValue New value for the margin in points. |
| */ |
| private setMargin_( |
| marginSide: CustomMarginsOrientation, marginValue: number) { |
| const oldMargins = this.getSettingValue('customMargins') as MarginsSetting; |
| const key = MARGIN_KEY_MAP.get(marginSide)!; |
| if (oldMargins[key] === marginValue) { |
| return; |
| } |
| const newMargins = Object.assign({}, oldMargins); |
| newMargins[key] = marginValue; |
| this.setSetting('customMargins', newMargins); |
| } |
| |
| /** |
| * @param marginSide The margin side. |
| * @param value The new margin value in points. |
| * @return The clipped margin value in points. |
| */ |
| private clipAndRoundValue_( |
| marginSide: CustomMarginsOrientation, value: number): number { |
| if (value < 0) { |
| return 0; |
| } |
| const Orientation = CustomMarginsOrientation; |
| let limit = 0; |
| const margins = this.getSettingValue('customMargins') as MarginsSetting; |
| 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 e Event containing the new textbox value. |
| */ |
| protected onTextChange_(e: CustomEvent<number>) { |
| const control = e.target as PrintPreviewMarginControlElement; |
| control.invalid = false; |
| const clippedValue = this.clipAndRoundValue_(control.side, e.detail); |
| control.setPositionInPts(clippedValue); |
| this.setMargin_(control.side, clippedValue); |
| } |
| |
| /** |
| * @param e Event fired when a control's text field is blurred. Contains |
| * information about whether the control is in an invalid state. |
| */ |
| protected onTextBlur_(e: CustomEvent<boolean>) { |
| const control = e.target as PrintPreviewMarginControlElement; |
| control.setTextboxValue(control.getPositionInPts()); |
| if (e.detail /* detail is true if the control is in an invalid state */) { |
| control.invalid = false; |
| } |
| this.textboxFocused_ = false; |
| } |
| |
| /** |
| * @param e Fired when pointerdown occurs on a margin control. |
| */ |
| protected onPointerDown_(e: PointerEvent) { |
| const control = e.target as PrintPreviewMarginControlElement; |
| if (!control.shouldDrag(e)) { |
| return; |
| } |
| |
| this.pointerStartPositionInPixels_ = new Coordinate2d(e.x, e.y); |
| this.marginStartPositionInPixels_ = |
| new Coordinate2d(control.offsetLeft, control.offsetTop); |
| this.dragging_ = this.isTopOrBottom_(control.side) ? 'dragging-vertical' : |
| 'dragging-horizontal'; |
| this.eventTracker_.add( |
| control, 'pointercancel', (e: PointerEvent) => this.onPointerUp_(e)); |
| this.eventTracker_.add( |
| control, 'pointerup', (e: PointerEvent) => this.onPointerUp_(e)); |
| this.eventTracker_.add( |
| control, 'pointermove', (e: PointerEvent) => this.onPointerMove_(e)); |
| control.setPointerCapture(e.pointerId); |
| |
| this.fireDragChanged_(true); |
| } |
| |
| /** |
| * @param dragChanged |
| */ |
| private fireDragChanged_(dragChanged: boolean) { |
| this.dispatchEvent(new CustomEvent( |
| 'margin-drag-changed', |
| {bubbles: true, composed: true, detail: dragChanged})); |
| } |
| |
| /** |
| * Set display:none after the opacity transition for the controls is done. |
| */ |
| protected onTransitionEnd_() { |
| if (this.invisible_) { |
| this.style.display = 'none'; |
| } |
| } |
| |
| /** |
| * Updates the translation transformation that translates pixel values in |
| * the space of the HTML DOM. |
| * @param translateTransform Updated value of the translation transformation. |
| */ |
| updateTranslationTransform(translateTransform: Coordinate2d) { |
| if (!translateTransform.equals(this.translateTransform_)) { |
| this.translateTransform_ = translateTransform; |
| } |
| } |
| |
| /** |
| * Updates the scaling transform that scales pixels values to point values. |
| * @param scaleTransform Updated value of the scale transform. |
| */ |
| updateScaleTransform(scaleTransform: number) { |
| if (scaleTransform !== this.scaleTransform_) { |
| this.scaleTransform_ = scaleTransform; |
| } |
| } |
| |
| /** |
| * Clips margin controls to the given clip size in pixels. |
| * @param clipSize Size to clip the margin controls to. |
| */ |
| updateClippingMask(clipSize: Size) { |
| this.clipSize_ = clipSize; |
| } |
| } |
| |
| export type MarginControlContainerElement = |
| PrintPreviewMarginControlContainerElement; |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'print-preview-margin-control-container': |
| PrintPreviewMarginControlContainerElement; |
| } |
| } |
| |
| customElements.define( |
| PrintPreviewMarginControlContainerElement.is, |
| PrintPreviewMarginControlContainerElement); |