1// Copyright (c) 2012 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 5cr.define('print_preview', function() { 6 'use strict'; 7 8 /** 9 * UI component used for setting custom print margins. 10 * @param {!print_preview.DocumentInfo} documentInfo Document data model. 11 * @param {!print_preview.ticket_items.MarginsType} marginsTypeTicketItem 12 * Used to read margins type. 13 * @param {!print_preview.ticket_items.CustomMargins} customMarginsTicketItem 14 * Used to read and write custom margin values. 15 * @param {!print_preview.MeasurementSystem} measurementSystem Used to convert 16 * between the system's local units and points. 17 * @constructor 18 * @extends {print_preview.Component} 19 */ 20 function MarginControlContainer(documentInfo, marginsTypeTicketItem, 21 customMarginsTicketItem, measurementSystem) { 22 print_preview.Component.call(this); 23 24 /** 25 * Document data model. 26 * @type {!print_preview.DocumentInfo} 27 * @private 28 */ 29 this.documentInfo_ = documentInfo; 30 31 /** 32 * Margins type ticket item used to read predefined margins type. 33 */ 34 this.marginsTypeTicketItem_ = marginsTypeTicketItem; 35 36 /** 37 * Custom margins ticket item used to read/write custom margin values. 38 * @type {!print_preview.ticket_items.CustomMargins} 39 * @private 40 */ 41 this.customMarginsTicketItem_ = customMarginsTicketItem; 42 43 /** 44 * Used to convert between the system's local units and points. 45 * @type {!print_preview.MeasurementSystem} 46 * @private 47 */ 48 this.measurementSystem_ = measurementSystem; 49 50 /** 51 * Convenience array that contains all of the margin controls. 52 * @type {!Object.< 53 * !print_preview.ticket_items.CustomMargins.Orientation, 54 * !print_preview.MarginControl>} 55 * @private 56 */ 57 this.controls_ = {}; 58 for (var key in print_preview.ticket_items.CustomMargins.Orientation) { 59 var orientation = print_preview.ticket_items.CustomMargins.Orientation[ 60 key]; 61 var control = new print_preview.MarginControl(orientation); 62 this.controls_[orientation] = control; 63 this.addChild(control); 64 } 65 66 /** 67 * Margin control currently being dragged. Null if no control is being 68 * dragged. 69 * @type {print_preview.MarginControl} 70 * @private 71 */ 72 this.draggedControl_ = null; 73 74 /** 75 * Translation transformation in pixels to translate from the origin of the 76 * custom margins component to the top-left corner of the most visible 77 * preview page. 78 * @type {!print_preview.Coordinate2d} 79 * @private 80 */ 81 this.translateTransform_ = new print_preview.Coordinate2d(0, 0); 82 83 /** 84 * Scaling transformation to scale from pixels to the units which the 85 * print preview is in. The scaling factor is the same in both dimensions, 86 * so this field is just a single number. 87 * @type {number} 88 * @private 89 */ 90 this.scaleTransform_ = 1; 91 92 /** 93 * Clipping size for clipping the margin controls. 94 * @type {print_preview.Size} 95 * @private 96 */ 97 this.clippingSize_ = null; 98 }; 99 100 /** 101 * CSS classes used by the custom margins component. 102 * @enum {string} 103 * @private 104 */ 105 MarginControlContainer.Classes_ = { 106 DRAGGING_HORIZONTAL: 'margin-control-container-dragging-horizontal', 107 DRAGGING_VERTICAL: 'margin-control-container-dragging-vertical' 108 }; 109 110 /** 111 * @param {!print_preview.ticket_items.CustomMargins.Orientation} orientation 112 * Orientation value to test. 113 * @return {boolean} Whether the given orientation is TOP or BOTTOM. 114 * @private 115 */ 116 MarginControlContainer.isTopOrBottom_ = function(orientation) { 117 return orientation == 118 print_preview.ticket_items.CustomMargins.Orientation.TOP || 119 orientation == 120 print_preview.ticket_items.CustomMargins.Orientation.BOTTOM; 121 }; 122 123 MarginControlContainer.prototype = { 124 __proto__: print_preview.Component.prototype, 125 126 /** 127 * Updates the translation transformation that translates pixel values in 128 * the space of the HTML DOM. 129 * @param {print_preview.Coordinate2d} translateTransform Updated value of 130 * the translation transformation. 131 */ 132 updateTranslationTransform: function(translateTransform) { 133 if (!translateTransform.equals(this.translateTransform_)) { 134 this.translateTransform_ = translateTransform; 135 for (var orientation in this.controls_) { 136 this.controls_[orientation].setTranslateTransform(translateTransform); 137 } 138 } 139 }, 140 141 /** 142 * Updates the scaling transform that scales pixels values to point values. 143 * @param {number} scaleTransform Updated value of the scale transform. 144 */ 145 updateScaleTransform: function(scaleTransform) { 146 if (scaleTransform != this.scaleTransform_) { 147 this.scaleTransform_ = scaleTransform; 148 for (var orientation in this.controls_) { 149 this.controls_[orientation].setScaleTransform(scaleTransform); 150 } 151 } 152 }, 153 154 /** 155 * Clips margin controls to the given clip size in pixels. 156 * @param {print_preview.Size} clipSize Size to clip the margin controls to. 157 */ 158 updateClippingMask: function(clipSize) { 159 if (!clipSize) { 160 return; 161 } 162 this.clippingSize_ = clipSize; 163 for (var orientation in this.controls_) { 164 var el = this.controls_[orientation].getElement(); 165 el.style.clip = 'rect(' + 166 (-el.offsetTop) + 'px, ' + 167 (clipSize.width - el.offsetLeft) + 'px, ' + 168 (clipSize.height - el.offsetTop) + 'px, ' + 169 (-el.offsetLeft) + 'px)'; 170 } 171 }, 172 173 /** Shows the margin controls if the need to be shown. */ 174 showMarginControlsIfNeeded: function() { 175 if (this.marginsTypeTicketItem_.getValue() == 176 print_preview.ticket_items.MarginsType.Value.CUSTOM) { 177 this.setIsMarginControlsVisible_(true); 178 } 179 }, 180 181 /** @override */ 182 enterDocument: function() { 183 print_preview.Component.prototype.enterDocument.call(this); 184 185 // We want to respond to mouse up events even beyond the component's 186 // element. 187 this.tracker.add(window, 'mouseup', this.onMouseUp_.bind(this)); 188 this.tracker.add(window, 'mousemove', this.onMouseMove_.bind(this)); 189 this.tracker.add( 190 this.getElement(), 'mouseover', this.onMouseOver_.bind(this)); 191 this.tracker.add( 192 this.getElement(), 'mouseout', this.onMouseOut_.bind(this)); 193 194 this.tracker.add( 195 this.documentInfo_, 196 print_preview.DocumentInfo.EventType.CHANGE, 197 this.onTicketChange_.bind(this)); 198 this.tracker.add( 199 this.marginsTypeTicketItem_, 200 print_preview.ticket_items.TicketItem.EventType.CHANGE, 201 this.onTicketChange_.bind(this)); 202 this.tracker.add( 203 this.customMarginsTicketItem_, 204 print_preview.ticket_items.TicketItem.EventType.CHANGE, 205 this.onTicketChange_.bind(this)); 206 207 for (var orientation in this.controls_) { 208 this.tracker.add( 209 this.controls_[orientation], 210 print_preview.MarginControl.EventType.DRAG_START, 211 this.onControlDragStart_.bind(this, this.controls_[orientation])); 212 this.tracker.add( 213 this.controls_[orientation], 214 print_preview.MarginControl.EventType.TEXT_CHANGE, 215 this.onControlTextChange_.bind(this, this.controls_[orientation])); 216 } 217 }, 218 219 /** @override */ 220 decorateInternal: function() { 221 for (var orientation in this.controls_) { 222 this.controls_[orientation].render(this.getElement()); 223 } 224 }, 225 226 /** 227 * @param {boolean} isVisible Whether the margin controls are visible. 228 * @private 229 */ 230 setIsMarginControlsVisible_: function(isVisible) { 231 for (var orientation in this.controls_) { 232 this.controls_[orientation].setIsVisible(isVisible); 233 } 234 }, 235 236 /** 237 * Moves the position of the given control to the desired position in 238 * pixels within some constraint minimum and maximum. 239 * @param {!print_preview.MarginControl} control Control to move. 240 * @param {!print_preview.Coordinate2d} posInPixels Desired position to move 241 * to in pixels. 242 * @private 243 */ 244 moveControlWithConstraints_: function(control, posInPixels) { 245 var newPosInPts; 246 if (MarginControlContainer.isTopOrBottom_(control.getOrientation())) { 247 newPosInPts = control.convertPixelsToPts(posInPixels.y); 248 } else { 249 newPosInPts = control.convertPixelsToPts(posInPixels.x); 250 } 251 newPosInPts = Math.min(this.customMarginsTicketItem_.getMarginMax( 252 control.getOrientation()), 253 newPosInPts); 254 newPosInPts = Math.max(0, newPosInPts); 255 newPosInPts = Math.round(newPosInPts); 256 control.setPositionInPts(newPosInPts); 257 control.setTextboxValue(this.serializeValueFromPts_(newPosInPts)); 258 }, 259 260 /** 261 * @param {string} value Value to parse to points. E.g. '3.40"' or '200mm'. 262 * @return {number} Value in points represented by the input value. 263 * @private 264 */ 265 parseValueToPts_: function(value) { 266 // Removing whitespace anywhere in the string. 267 value = value.replace(/\s*/g, ''); 268 if (value.length == 0) { 269 return null; 270 } 271 var validationRegex = new RegExp('^(^-?)(\\d)+(\\' + 272 this.measurementSystem_.thousandsDelimeter + '\\d{3})*(\\' + 273 this.measurementSystem_.decimalDelimeter + '\\d*)?' + 274 '(' + this.measurementSystem_.unitSymbol + ')?$'); 275 if (validationRegex.test(value)) { 276 // Replacing decimal point with the dot symbol in order to use 277 // parseFloat() properly. 278 var replacementRegex = 279 new RegExp('\\' + this.measurementSystem_.decimalDelimeter + '{1}'); 280 value = value.replace(replacementRegex, '.'); 281 return this.measurementSystem_.convertToPoints(parseFloat(value)); 282 } 283 return null; 284 }, 285 286 /** 287 * @param {number} value Value in points to serialize. 288 * @return {string} String representation of the value in the system's local 289 * units. 290 * @private 291 */ 292 serializeValueFromPts_: function(value) { 293 value = this.measurementSystem_.convertFromPoints(value); 294 value = this.measurementSystem_.roundValue(value); 295 return value + this.measurementSystem_.unitSymbol; 296 }, 297 298 /** 299 * Called when a margin control starts to drag. 300 * @param {print_preview.MarginControl} control The control which started to 301 * drag. 302 * @private 303 */ 304 onControlDragStart_: function(control) { 305 this.draggedControl_ = control; 306 this.getElement().classList.add( 307 MarginControlContainer.isTopOrBottom_(control.getOrientation()) ? 308 MarginControlContainer.Classes_.DRAGGING_VERTICAL : 309 MarginControlContainer.Classes_.DRAGGING_HORIZONTAL); 310 }, 311 312 /** 313 * Called when the mouse moves in the custom margins component. Moves the 314 * dragged margin control. 315 * @param {MouseEvent} event Contains the position of the mouse. 316 * @private 317 */ 318 onMouseMove_: function(event) { 319 if (this.draggedControl_) { 320 this.moveControlWithConstraints_( 321 this.draggedControl_, 322 this.draggedControl_.translateMouseToPositionInPixels( 323 new print_preview.Coordinate2d(event.x, event.y))); 324 this.updateClippingMask(this.clippingSize_); 325 } 326 }, 327 328 /** 329 * Called when the mouse is released in the custom margins component. 330 * Releases the dragged margin control. 331 * @param {MouseEvent} event Contains the position of the mouse. 332 * @private 333 */ 334 onMouseUp_: function(event) { 335 if (this.draggedControl_) { 336 this.getElement().classList.remove( 337 MarginControlContainer.Classes_.DRAGGING_VERTICAL); 338 this.getElement().classList.remove( 339 MarginControlContainer.Classes_.DRAGGING_HORIZONTAL); 340 if (event) { 341 var posInPixels = 342 this.draggedControl_.translateMouseToPositionInPixels( 343 new print_preview.Coordinate2d(event.x, event.y)); 344 this.moveControlWithConstraints_(this.draggedControl_, posInPixels); 345 } 346 this.updateClippingMask(this.clippingSize_); 347 this.customMarginsTicketItem_.updateMargin( 348 this.draggedControl_.getOrientation(), 349 this.draggedControl_.getPositionInPts()); 350 this.draggedControl_ = null; 351 } 352 }, 353 354 /** 355 * Called when the mouse moves onto the component. Shows the margin 356 * controls. 357 * @private 358 */ 359 onMouseOver_: function() { 360 var fromElement = event.fromElement; 361 while (fromElement != null) { 362 if (fromElement == this.getElement()) { 363 return; 364 } 365 fromElement = fromElement.parentElement; 366 } 367 if (this.marginsTypeTicketItem_.isCapabilityAvailable() && 368 this.marginsTypeTicketItem_.getValue() == 369 print_preview.ticket_items.MarginsType.Value.CUSTOM) { 370 this.setIsMarginControlsVisible_(true); 371 } 372 }, 373 374 /** 375 * Called when the mouse moves off of the component. Hides the margin 376 * controls. 377 * @private 378 */ 379 onMouseOut_: function(event) { 380 var toElement = event.toElement; 381 while (toElement != null) { 382 if (toElement == this.getElement()) { 383 return; 384 } 385 toElement = toElement.parentElement; 386 } 387 if (this.draggedControl_ != null) { 388 return; 389 } 390 for (var orientation in this.controls_) { 391 if (this.controls_[orientation].getIsFocused() || 392 this.controls_[orientation].getIsInError()) { 393 return; 394 } 395 } 396 this.setIsMarginControlsVisible_(false); 397 }, 398 399 /** 400 * Called when the print ticket changes. Updates the position of the margin 401 * controls. 402 * @private 403 */ 404 onTicketChange_: function() { 405 var margins = this.customMarginsTicketItem_.getValue(); 406 for (var orientation in this.controls_) { 407 var control = this.controls_[orientation]; 408 control.setPageSize(this.documentInfo_.pageSize); 409 control.setTextboxValue( 410 this.serializeValueFromPts_(margins.get(orientation))); 411 control.setPositionInPts(margins.get(orientation)); 412 control.setIsInError(false); 413 control.setIsEnabled(true); 414 } 415 this.updateClippingMask(this.clippingSize_); 416 if (this.marginsTypeTicketItem_.getValue() != 417 print_preview.ticket_items.MarginsType.Value.CUSTOM) { 418 this.setIsMarginControlsVisible_(false); 419 } 420 }, 421 422 /** 423 * Called when the text in a textbox of a margin control changes or the 424 * textbox loses focus. 425 * Updates the print ticket store. 426 * @param {!print_preview.MarginControl} control Updated control. 427 * @private 428 */ 429 onControlTextChange_: function(control) { 430 var marginValue = this.parseValueToPts_(control.getTextboxValue()); 431 if (marginValue != null) { 432 this.customMarginsTicketItem_.updateMargin( 433 control.getOrientation(), marginValue); 434 // Enable all controls. 435 for (var o in this.controls_) { 436 this.controls_[o].setIsEnabled(true); 437 } 438 control.setIsInError(false); 439 } else { 440 var enableOtherControls; 441 if (!control.getIsFocused()) { 442 // If control no longer in focus, revert to previous valid value. 443 control.setTextboxValue( 444 this.serializeValueFromPts_(control.getPositionInPts())); 445 control.setIsInError(false); 446 enableOtherControls = true; 447 } else { 448 control.setIsInError(true); 449 enableOtherControls = false; 450 } 451 // Enable other controls. 452 for (var o in this.controls_) { 453 if (control.getOrientation() != o) { 454 this.controls_[o].setIsEnabled(enableOtherControls); 455 } 456 } 457 } 458 } 459 }; 460 461 // Export 462 return { 463 MarginControlContainer: MarginControlContainer 464 }; 465}); 466