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   * Draggable control for setting a page margin.
10   * @param {!print_preview.ticket_items.CustomMargins.Orientation} orientation
11   *     Orientation of the margin control that determines where the margin
12   *     textbox will be placed.
13   * @constructor
14   * @extends {print_preview.Component}
15   */
16  function MarginControl(orientation) {
17    print_preview.Component.call(this);
18
19    /**
20     * Determines where the margin textbox will be placed.
21     * @type {!print_preview.ticket_items.CustomMargins.Orientation}
22     * @private
23     */
24    this.orientation_ = orientation;
25
26    /**
27     * Position of the margin control in points.
28     * @type {number}
29     * @private
30     */
31    this.positionInPts_ = 0;
32
33    /**
34     * Page size of the document to print.
35     * @type {!print_preview.Size}
36     * @private
37     */
38    this.pageSize_ = new print_preview.Size(0, 0);
39
40    /**
41     * Amount to scale pixel values by to convert to pixel space.
42     * @type {number}
43     * @private
44     */
45    this.scaleTransform_ = 1;
46
47    /**
48     * Amount to translate values in pixel space.
49     * @type {!print_preview.Coordinate2d}
50     * @private
51     */
52    this.translateTransform_ = new print_preview.Coordinate2d(0, 0);
53
54    /**
55     * Position of the margin control when dragging starts.
56     * @type {print_preview.Coordinate2d}
57     * @private
58     */
59    this.marginStartPositionInPixels_ = null;
60
61    /**
62     * Position of the mouse when the dragging starts.
63     * @type {print_preview.Coordinate2d}
64     * @private
65     */
66    this.mouseStartPositionInPixels_ = null;
67
68    /**
69     * Processing timeout for the textbox.
70     * @type {?number}
71     * @private
72     */
73    this.textTimeout_ = null;
74
75    /**
76     * Textbox used to display and receive the value of the margin.
77     * @type {HTMLInputElement}
78     * @private
79     */
80    this.textbox_ = null;
81
82    /**
83     * Element of the margin control line.
84     * @type {HTMLElement}
85     * @private
86     */
87    this.marginLineEl_ = null;
88
89    /**
90     * Whether this margin control's textbox has keyboard focus.
91     * @type {boolean}
92     * @private
93     */
94    this.isFocused_ = false;
95
96    /**
97     * Whether the margin control is in an error state.
98     * @type {boolean}
99     * @private
100     */
101    this.isInError_ = false;
102  };
103
104  /**
105   * Event types dispatched by the margin control.
106   * @enum {string}
107   */
108  MarginControl.EventType = {
109    // Dispatched when the margin control starts dragging.
110    DRAG_START: 'print_preview.MarginControl.DRAG_START',
111
112    // Dispatched when the text in the margin control's textbox changes.
113    TEXT_CHANGE: 'print_preview.MarginControl.TEXT_CHANGE'
114  };
115
116  /**
117   * CSS classes used by this component.
118   * @enum {string}
119   * @private
120   */
121  MarginControl.Classes_ = {
122    TEXTBOX: 'margin-control-textbox',
123    DRAGGING: 'margin-control-dragging',
124    LINE: 'margin-control-line'
125  };
126
127  /**
128   * Radius of the margin control in pixels. Padding of control + 1 for border.
129   * @type {number}
130   * @const
131   * @private
132   */
133  MarginControl.RADIUS_ = 9;
134
135  /**
136   * Timeout after a text change after which the text in the textbox is saved to
137   * the print ticket. Value in milliseconds.
138   * @type {number}
139   * @const
140   * @private
141   */
142  MarginControl.TEXTBOX_TIMEOUT_ = 1000;
143
144  MarginControl.prototype = {
145    __proto__: print_preview.Component.prototype,
146
147    /** @return {boolean} Whether this margin control is in focus. */
148    getIsFocused: function() {
149      return this.isFocused_;
150    },
151
152    /**
153     * @return {!print_preview.ticket_items.CustomMargins.Orientation}
154     *     Orientation of the margin control.
155     */
156    getOrientation: function() {
157      return this.orientation_;
158    },
159
160    /**
161     * @param {number} scaleTransform New scale transform of the margin control.
162     */
163    setScaleTransform: function(scaleTransform) {
164      this.scaleTransform_ = scaleTransform;
165      // Reset position
166      this.setPositionInPts(this.positionInPts_);
167    },
168
169    /**
170     * @param {!print_preview.Coordinate2d} translateTransform New translate
171     *     transform of the margin control.
172     */
173    setTranslateTransform: function(translateTransform) {
174      this.translateTransform_ = translateTransform;
175      // Reset position
176      this.setPositionInPts(this.positionInPts_);
177    },
178
179    /**
180     * @param {!print_preview.Size} pageSize New size of the document's pages.
181     */
182    setPageSize: function(pageSize) {
183      this.pageSize_ = pageSize;
184      this.setPositionInPts(this.positionInPts_);
185    },
186
187    /** @param {boolean} isVisible Whether the margin control is visible. */
188    setIsVisible: function(isVisible) {
189      this.getElement().classList.toggle('invisible', !isVisible);
190    },
191
192    /** @return {boolean} Whether the margin control is in an error state. */
193    getIsInError: function() {
194      return this.isInError_;
195    },
196
197    /**
198     * @param {boolean} isInError Whether the margin control is in an error
199     *     state.
200     */
201    setIsInError: function(isInError) {
202      this.isInError_ = isInError;
203      this.textbox_.classList.toggle('invalid', isInError);
204    },
205
206    /** @param {boolean} isEnabled Whether to enable the margin control. */
207    setIsEnabled: function(isEnabled) {
208      this.textbox_.disabled = !isEnabled;
209      this.getElement().classList.toggle('margin-control-disabled', !isEnabled);
210    },
211
212    /** @return {number} Current position of the margin control in points. */
213    getPositionInPts: function() {
214      return this.positionInPts_;
215    },
216
217    /**
218     * @param {number} posInPts New position of the margin control in points.
219     */
220    setPositionInPts: function(posInPts) {
221      this.positionInPts_ = posInPts;
222      var orientationEnum =
223          print_preview.ticket_items.CustomMargins.Orientation;
224      var x = this.translateTransform_.x;
225      var y = this.translateTransform_.y;
226      var width = null, height = null;
227      if (this.orientation_ == orientationEnum.TOP) {
228        y = this.scaleTransform_ * posInPts + this.translateTransform_.y -
229            MarginControl.RADIUS_;
230        width = this.scaleTransform_ * this.pageSize_.width;
231      } else if (this.orientation_ == orientationEnum.RIGHT) {
232        x = this.scaleTransform_ * (this.pageSize_.width - posInPts) +
233            this.translateTransform_.x - MarginControl.RADIUS_;
234        height = this.scaleTransform_ * this.pageSize_.height;
235      } else if (this.orientation_ == orientationEnum.BOTTOM) {
236        y = this.scaleTransform_ * (this.pageSize_.height - posInPts) +
237            this.translateTransform_.y - MarginControl.RADIUS_;
238        width = this.scaleTransform_ * this.pageSize_.width;
239      } else {
240        x = this.scaleTransform_ * posInPts + this.translateTransform_.x -
241            MarginControl.RADIUS_;
242        height = this.scaleTransform_ * this.pageSize_.height;
243      }
244      this.getElement().style.left = Math.round(x) + 'px';
245      this.getElement().style.top = Math.round(y) + 'px';
246      if (width != null) {
247        this.getElement().style.width = Math.round(width) + 'px';
248      }
249      if (height != null) {
250        this.getElement().style.height = Math.round(height) + 'px';
251      }
252    },
253
254    /** @return {string} The value in the margin control's textbox. */
255    getTextboxValue: function() {
256      return this.textbox_.value;
257    },
258
259    /** @param {string} value New value of the margin control's textbox. */
260    setTextboxValue: function(value) {
261      if (this.textbox_.value != value) {
262        this.textbox_.value = value;
263      }
264    },
265
266    /**
267     * Converts a value in pixels to points.
268     * @param {number} pixels Pixel value to convert.
269     * @return {number} Given value expressed in points.
270     */
271    convertPixelsToPts: function(pixels) {
272      var pts;
273      var orientationEnum =
274          print_preview.ticket_items.CustomMargins.Orientation;
275      if (this.orientation_ == orientationEnum.TOP) {
276        pts = pixels - this.translateTransform_.y + MarginControl.RADIUS_;
277        pts /= this.scaleTransform_;
278      } else if (this.orientation_ == orientationEnum.RIGHT) {
279        pts = pixels - this.translateTransform_.x + MarginControl.RADIUS_;
280        pts /= this.scaleTransform_;
281        pts = this.pageSize_.width - pts;
282      } else if (this.orientation_ == orientationEnum.BOTTOM) {
283        pts = pixels - this.translateTransform_.y + MarginControl.RADIUS_;
284        pts /= this.scaleTransform_;
285        pts = this.pageSize_.height - pts;
286      } else {
287        pts = pixels - this.translateTransform_.x + MarginControl.RADIUS_;
288        pts /= this.scaleTransform_;
289      }
290      return pts;
291    },
292
293    /**
294     * Translates the position of the margin control relative to the mouse
295     * position in pixels.
296     * @param {!print_preview.Coordinate2d} mousePosition New position of
297     *     the mouse.
298     * @return {!print_preview.Coordinate2d} New position of the margin control.
299     */
300    translateMouseToPositionInPixels: function(mousePosition) {
301      return new print_preview.Coordinate2d(
302          mousePosition.x - this.mouseStartPositionInPixels_.x +
303              this.marginStartPositionInPixels_.x,
304          mousePosition.y - this.mouseStartPositionInPixels_.y +
305              this.marginStartPositionInPixels_.y);
306    },
307
308    /** @override */
309    createDom: function() {
310      this.setElementInternal(this.cloneTemplateInternal(
311          'margin-control-template'));
312      this.getElement().classList.add('margin-control-' + this.orientation_);
313      this.textbox_ = this.getElement().getElementsByClassName(
314          MarginControl.Classes_.TEXTBOX)[0];
315      this.textbox_.setAttribute(
316          'aria-label', loadTimeData.getString(this.orientation_));
317      this.marginLineEl_ = this.getElement().getElementsByClassName(
318          MarginControl.Classes_.LINE)[0];
319    },
320
321    /** @override */
322    enterDocument: function() {
323      print_preview.Component.prototype.enterDocument.call(this);
324      this.tracker.add(
325          this.getElement(), 'mousedown', this.onMouseDown_.bind(this));
326      this.tracker.add(
327          this.getElement(),
328          'webkitTransitionEnd',
329          this.onWebkitTransitionEnd_.bind(this));
330      this.tracker.add(
331          this.textbox_, 'input', this.onTextboxInput_.bind(this));
332      this.tracker.add(
333          this.textbox_, 'keydown', this.onTextboxKeyDown_.bind(this));
334      this.tracker.add(
335          this.textbox_, 'focus', this.setIsFocused_.bind(this, true));
336      this.tracker.add(this.textbox_, 'blur', this.onTexboxBlur_.bind(this));
337    },
338
339    /** @override */
340    exitDocument: function() {
341      print_preview.Component.prototype.exitDocument.call(this);
342      this.textbox_ = null;
343      this.marginLineEl_ = null;
344    },
345
346    /**
347     * @param {boolean} isFocused Whether the margin control is in focus.
348     * @private
349     */
350    setIsFocused_: function(isFocused) {
351      this.isFocused_ = isFocused;
352    },
353
354    /**
355     * Called whenever a mousedown event occurs on the component.
356     * @param {MouseEvent} event The event that occured.
357     * @private
358     */
359    onMouseDown_: function(event) {
360      if (!this.textbox_.disabled &&
361          event.button == 0 &&
362          (event.target == this.getElement() ||
363              event.target == this.marginLineEl_)) {
364        this.mouseStartPositionInPixels_ =
365            new print_preview.Coordinate2d(event.x, event.y);
366        this.marginStartPositionInPixels_ = new print_preview.Coordinate2d(
367            this.getElement().offsetLeft, this.getElement().offsetTop);
368        this.setIsInError(false);
369        cr.dispatchSimpleEvent(this, MarginControl.EventType.DRAG_START);
370      }
371    },
372
373    /**
374     * Called when opacity CSS transition ends.
375     * @private
376     */
377    onWebkitTransitionEnd_: function(event) {
378      if (event.propertyName != 'opacity')
379        return;
380      var elStyle = window.getComputedStyle(this.getElement());
381      var opacity = parseInt(elStyle.getPropertyValue('opacity'));
382      this.textbox_.setAttribute('aria-hidden', opacity == 0);
383    },
384
385    /**
386     * Called when textbox content changes. Starts text change timeout.
387     * @private
388     */
389    onTextboxInput_: function(event) {
390      if (this.textTimeout_) {
391        clearTimeout(this.textTimeout_);
392        this.textTimeout_ = null;
393      }
394      this.textTimeout_ = setTimeout(
395          this.onTextboxTimeout_.bind(this), MarginControl.TEXTBOX_TIMEOUT_);
396    },
397
398    /**
399     * Called when a key down event occurs on the textbox. Dispatches a
400     * TEXT_CHANGE event if the "Enter" key was pressed.
401     * @param {Event} event Contains the key that was pressed.
402     * @private
403     */
404    onTextboxKeyDown_: function(event) {
405      if (this.textTimeout_) {
406        clearTimeout(this.textTimeout_);
407        this.textTimeout_ = null;
408      }
409      if (event.keyCode == 13 /*enter*/) {
410        cr.dispatchSimpleEvent(this, MarginControl.EventType.TEXT_CHANGE);
411      }
412    },
413
414    /**
415     * Called after a timeout after the text in the textbox has changed. Saves
416     * the textbox's value to the print ticket.
417     * @private
418     */
419    onTextboxTimeout_: function() {
420      this.textTimeout_ = null;
421      cr.dispatchSimpleEvent(this, MarginControl.EventType.TEXT_CHANGE);
422    },
423
424    /**
425     * Called when the textbox loses focus. Dispatches a TEXT_CHANGE event.
426     */
427    onTexboxBlur_: function() {
428      if (this.textTimeout_) {
429        clearTimeout(this.textTimeout_);
430        this.textTimeout_ = null;
431      }
432      this.setIsFocused_(false);
433      cr.dispatchSimpleEvent(this, MarginControl.EventType.TEXT_CHANGE);
434    }
435  };
436
437  // Export
438  return {
439    MarginControl: MarginControl
440  };
441});
442