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