blob: 883e7eaa6a5589fee8574db18054c06850cda61b [file] [log] [blame]
// 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.
import 'chrome://resources/cr_elements/shared_vars_css.m.js';
import 'chrome://resources/cr_elements/cr_input/cr_input_style_css.m.js';
import '../strings.m.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {I18nBehavior} from 'chrome://resources/js/i18n_behavior.m.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {Coordinate2d} from '../data/coordinate2d.js';
import {CustomMarginsOrientation} from '../data/margins.js';
import {MeasurementSystem} from '../data/measurement_system.js';
import {Size} from '../data/size.js';
import {observerDepsDefined} from '../print_preview_utils.js';
import {InputBehavior} from './input_behavior.js';
/**
* Radius of the margin control in pixels. Padding of control + 1 for border.
* @type {number}
*/
const RADIUS_PX = 9;
Polymer({
is: 'print-preview-margin-control',
_template: html`{__html_template__}`,
behaviors: [InputBehavior, I18nBehavior],
properties: {
disabled: {
type: Boolean,
reflectToAttribute: true,
observer: 'onDisabledChange_',
},
side: {
type: String,
reflectToAttribute: true,
},
invalid: {
type: Boolean,
reflectToAttribute: true,
},
invisible: {
type: Boolean,
reflectToAttribute: true,
observer: 'onClipSizeChange_',
},
/** @type {?MeasurementSystem} */
measurementSystem: Object,
/** @private {boolean} */
focused_: {
type: Boolean,
reflectToAttribute: true,
value: false,
},
/** @private {number} */
positionInPts_: {
type: Number,
notify: true,
value: 0,
},
/** @type {number} */
scaleTransform: {
type: Number,
notify: true,
},
/** @type {!Coordinate2d} */
translateTransform: {
type: Object,
notify: true,
},
/** @type {!Size} */
pageSize: {
type: Object,
notify: true,
},
/** @type {?Size} */
clipSize: {
type: Object,
notify: true,
observer: 'onClipSizeChange_',
},
},
observers:
['updatePosition_(positionInPts_, scaleTransform, translateTransform, ' +
'pageSize, side)'],
listeners: {
'input-change': 'onInputChange_',
},
/** @return {!HTMLInputElement} The input element for InputBehavior. */
getInput() {
return /** @type {!HTMLInputElement} */ (this.$.input);
},
/**
* @param {number} valueInPts New value of the margin control's textbox in
* pts.
*/
setTextboxValue(valueInPts) {
const textbox = this.$.input;
const pts = textbox.value ? this.parseValueToPts_(textbox.value) : null;
if (pts !== null && valueInPts === Math.round(pts)) {
// If the textbox's value represents the same value in pts as the new one,
// don't reset. This allows the "undo" command to work as expected, see
// https://crbug.com/452844.
return;
}
textbox.value = this.serializeValueFromPts_(valueInPts);
this.resetString();
},
/** @return {number} The current position of the margin control. */
getPositionInPts() {
return this.positionInPts_;
},
/** @param {number} position The new position for the margin control. */
setPositionInPts(position) {
this.positionInPts_ = position;
},
/**
* @return {string} 'true' or 'false', indicating whether the input should be
* aria-hidden.
* @private
*/
getAriaHidden_() {
return this.invisible.toString();
},
/**
* Converts a value in pixels to points.
* @param {number} pixels Pixel value to convert.
* @return {number} Given value expressed in points.
*/
convertPixelsToPts(pixels) {
let pts;
const Orientation = CustomMarginsOrientation;
if (this.side === Orientation.TOP) {
pts = pixels - this.translateTransform.y + RADIUS_PX;
pts /= this.scaleTransform;
} else if (this.side === Orientation.RIGHT) {
pts = pixels - this.translateTransform.x + RADIUS_PX;
pts /= this.scaleTransform;
pts = this.pageSize.width - pts;
} else if (this.side === Orientation.BOTTOM) {
pts = pixels - this.translateTransform.y + RADIUS_PX;
pts /= this.scaleTransform;
pts = this.pageSize.height - pts;
} else {
assert(this.side === Orientation.LEFT);
pts = pixels - this.translateTransform.x + RADIUS_PX;
pts /= this.scaleTransform;
}
return pts;
},
/**
* @param {!PointerEvent} event A pointerdown event triggered by this element.
* @return {boolean} Whether the margin should start being dragged.
*/
shouldDrag(event) {
return !this.disabled && event.button === 0 &&
(event.path[0] === this.$.lineContainer ||
event.path[0] === this.$.line);
},
/** @private */
onDisabledChange_() {
if (this.disabled) {
this.focused_ = false;
}
},
/**
* @param {string} value Value to parse to points. E.g. '3.40' or '200'.
* @return {?number} Value in points represented by the input value.
* @private
*/
parseValueToPts_(value) {
value = value.trim();
if (value.length === 0) {
return null;
}
assert(this.measurementSystem);
const decimal = this.measurementSystem.decimalDelimiter;
const thousands = this.measurementSystem.thousandsDelimiter;
const whole = `(?:0|[1-9]\\d*|[1-9]\\d{0,2}(?:[${thousands}]\\d{3})*)`;
const fractional = `(?:[${decimal}]\\d*)`;
const validationRegex =
new RegExp(`^-?(?:${whole}${fractional}?|${fractional})$`);
if (validationRegex.test(value)) {
// Removing thousands delimiters and replacing the decimal delimiter with
// the dot symbol in order to use parseFloat() properly.
value = value.replace(new RegExp(`\\${thousands}`, 'g'), '')
.replace(decimal, '.');
return this.measurementSystem.convertToPoints(parseFloat(value));
}
return null;
},
/**
* @param {number} value Value in points to serialize.
* @return {string} String representation of the value in the system's local
* units.
* @private
*/
serializeValueFromPts_(value) {
assert(this.measurementSystem);
value = this.measurementSystem.convertFromPoints(value);
value = this.measurementSystem.roundValue(value);
// Convert the dot symbol to the decimal delimiter for the locale.
return value.toString().replace(
'.', this.measurementSystem.decimalDelimiter);
},
/**
* @param {!CustomEvent<string|undefined>} e Contains the new value of the
* input.
* @private
*/
onInputChange_(e) {
if (e.detail === '' || e.detail === undefined) {
return;
}
const value = this.parseValueToPts_(e.detail);
if (value === null) {
this.invalid = true;
return;
}
this.fire('text-change', value);
},
/** @private */
onBlur_() {
this.focused_ = false;
this.resetAndUpdate();
this.fire('text-blur', this.invalid || !this.$.input.value);
},
/** @private */
onFocus_() {
this.focused_ = true;
this.fire('text-focus');
},
/** @private */
updatePosition_() {
if (!observerDepsDefined(Array.from(arguments))) {
return;
}
const Orientation = CustomMarginsOrientation;
let x = this.translateTransform.x;
let y = this.translateTransform.y;
let width = null;
let height = null;
if (this.side === Orientation.TOP) {
y = this.scaleTransform * this.positionInPts_ +
this.translateTransform.y - RADIUS_PX;
width = this.scaleTransform * this.pageSize.width;
} else if (this.side === Orientation.RIGHT) {
x = this.scaleTransform * (this.pageSize.width - this.positionInPts_) +
this.translateTransform.x - RADIUS_PX;
height = this.scaleTransform * this.pageSize.height;
} else if (this.side === Orientation.BOTTOM) {
y = this.scaleTransform * (this.pageSize.height - this.positionInPts_) +
this.translateTransform.y - RADIUS_PX;
width = this.scaleTransform * this.pageSize.width;
} else {
x = this.scaleTransform * this.positionInPts_ +
this.translateTransform.x - RADIUS_PX;
height = this.scaleTransform * this.pageSize.height;
}
window.requestAnimationFrame(() => {
this.style.left = Math.round(x) + 'px';
this.style.top = Math.round(y) + 'px';
if (width !== null) {
this.style.width = Math.round(width) + 'px';
}
if (height !== null) {
this.style.height = Math.round(height) + 'px';
}
});
this.onClipSizeChange_();
},
/** @private */
onClipSizeChange_() {
if (!this.clipSize) {
return;
}
window.requestAnimationFrame(() => {
const offsetLeft = this.offsetLeft;
const offsetTop = this.offsetTop;
this.style.clip = 'rect(' + (-offsetTop) + 'px, ' +
(this.clipSize.width - offsetLeft) + 'px, ' +
(this.clipSize.height - offsetTop) + 'px, ' + (-offsetLeft) + 'px)';
});
},
});