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
5/**
6 * @fileoverview Nav dot
7 * This is the class for the navigation controls that appear along the bottom
8 * of the NTP.
9 */
10
11cr.define('ntp', function() {
12  'use strict';
13
14  /**
15   * Creates a new navigation dot.
16   * @param {TilePage} page The associated TilePage.
17   * @param {string} title The title of the navigation dot.
18   * @param {bool} titleIsEditable If true, the title can be changed.
19   * @param {bool} animate If true, animates into existence.
20   * @constructor
21   * @extends {HTMLLIElement}
22   */
23  function NavDot(page, title, titleIsEditable, animate) {
24    var dot = cr.doc.createElement('li');
25    dot.__proto__ = NavDot.prototype;
26    dot.initialize(page, title, titleIsEditable, animate);
27
28    return dot;
29  }
30
31  NavDot.prototype = {
32    __proto__: HTMLLIElement.prototype,
33
34    initialize: function(page, title, titleIsEditable, animate) {
35      this.className = 'dot';
36      this.setAttribute('role', 'button');
37
38      this.page_ = page;
39
40      var selectionBar = this.ownerDocument.createElement('div');
41      selectionBar.className = 'selection-bar';
42      this.appendChild(selectionBar);
43
44      // TODO(estade): should there be some limit to the number of characters?
45      this.input_ = this.ownerDocument.createElement('input');
46      this.input_.setAttribute('spellcheck', false);
47      this.input_.value = title;
48      // Take the input out of the tab-traversal focus order.
49      this.input_.disabled = true;
50      this.appendChild(this.input_);
51
52      this.displayTitle = title;
53      this.titleIsEditable_ = titleIsEditable;
54
55      this.addEventListener('keydown', this.onKeyDown_);
56      this.addEventListener('click', this.onClick_);
57      this.addEventListener('dblclick', this.onDoubleClick_);
58      this.dragWrapper_ = new cr.ui.DragWrapper(this, this);
59      this.addEventListener('webkitTransitionEnd', this.onTransitionEnd_);
60
61      this.input_.addEventListener('blur', this.onInputBlur_.bind(this));
62      this.input_.addEventListener('mousedown',
63                                   this.onInputMouseDown_.bind(this));
64      this.input_.addEventListener('keydown', this.onInputKeyDown_.bind(this));
65
66      if (animate) {
67        this.classList.add('small');
68        var self = this;
69        window.setTimeout(function() {
70          self.classList.remove('small');
71        }, 0);
72      }
73    },
74
75    /**
76     * @return {TilePage} The associated TilePage.
77     */
78    get page() {
79      return this.page_;
80    },
81
82    /**
83     * Sets/gets the display title.
84     * @type {string} title The display name for this nav dot.
85     */
86    get displayTitle() {
87      return this.title;
88    },
89    set displayTitle(title) {
90      this.title = this.input_.value = title;
91    },
92
93    /**
94     * Removes the dot from the page. If |opt_animate| is truthy, we first
95     * transition the element to 0 width.
96     * @param {boolean=} opt_animate Whether to animate the removal or not.
97     */
98    remove: function(opt_animate) {
99      if (opt_animate)
100        this.classList.add('small');
101      else
102        this.parentNode.removeChild(this);
103    },
104
105    /**
106     * Navigates the card slider to the page for this dot.
107     */
108    switchToPage: function() {
109      ntp.getCardSlider().selectCardByValue(this.page_, true);
110    },
111
112    /**
113     * Handler for keydown event on the dot.
114     * @param {Event} e The KeyboardEvent.
115     */
116    onKeyDown_: function(e) {
117      if (e.keyIdentifier == 'Enter') {
118        this.onClick_(e);
119        e.stopPropagation();
120      }
121    },
122
123    /**
124     * Clicking causes the associated page to show.
125     * @param {Event} e The click event.
126     * @private
127     */
128    onClick_: function(e) {
129      this.switchToPage();
130      // The explicit focus call is necessary because of overriding the default
131      // handling in onInputMouseDown_.
132      if (this.ownerDocument.activeElement != this.input_)
133        this.focus();
134
135      e.stopPropagation();
136    },
137
138    /**
139     * Double clicks allow the user to edit the page title.
140     * @param {Event} e The click event.
141     * @private
142     */
143    onDoubleClick_: function(e) {
144      if (this.titleIsEditable_) {
145        this.input_.disabled = false;
146        this.input_.focus();
147        this.input_.select();
148      }
149    },
150
151    /**
152     * Prevent mouse down on the input from selecting it.
153     * @param {Event} e The click event.
154     * @private
155     */
156    onInputMouseDown_: function(e) {
157      if (this.ownerDocument.activeElement != this.input_)
158        e.preventDefault();
159    },
160
161    /**
162     * Handle keypresses on the input.
163     * @param {Event} e The click event.
164     * @private
165     */
166    onInputKeyDown_: function(e) {
167      switch (e.keyIdentifier) {
168        case 'U+001B':  // Escape cancels edits.
169          this.input_.value = this.displayTitle;
170        case 'Enter':  // Fall through.
171          this.input_.blur();
172          break;
173      }
174    },
175
176    /**
177     * When the input blurs, commit the edited changes.
178     * @param {Event} e The blur event.
179     * @private
180     */
181    onInputBlur_: function(e) {
182      window.getSelection().removeAllRanges();
183      this.displayTitle = this.input_.value;
184      ntp.saveAppPageName(this.page_, this.displayTitle);
185      this.input_.disabled = true;
186    },
187
188    shouldAcceptDrag: function(e) {
189      return this.page_.shouldAcceptDrag(e);
190    },
191
192    /**
193     * A drag has entered the navigation dot. If the user hovers long enough,
194     * we will navigate to the relevant page.
195     * @param {Event} e The MouseOver event for the drag.
196     * @private
197     */
198    doDragEnter: function(e) {
199      var self = this;
200      function navPageClearTimeout() {
201        self.switchToPage();
202        self.dragNavTimeout = null;
203      }
204      this.dragNavTimeout = window.setTimeout(navPageClearTimeout, 500);
205
206      this.doDragOver(e);
207    },
208
209    /**
210     * A dragged element has moved over the navigation dot. Show the correct
211     * indicator and prevent default handling so the <input> won't act as a drag
212     * target.
213     * @param {Event} e The MouseOver event for the drag.
214     * @private
215     */
216    doDragOver: function(e) {
217      e.preventDefault();
218
219      if (!this.dragWrapper_.isCurrentDragTarget)
220        ntp.setCurrentDropEffect(e.dataTransfer, 'none');
221      else
222        this.page_.setDropEffect(e.dataTransfer);
223    },
224
225    /**
226     * A dragged element has been dropped on the navigation dot. Tell the page
227     * to append it.
228     * @param {Event} e The MouseOver event for the drag.
229     * @private
230     */
231    doDrop: function(e) {
232      e.stopPropagation();
233      var tile = ntp.getCurrentlyDraggingTile();
234      if (tile && tile.tilePage != this.page_)
235        this.page_.appendDraggingTile();
236      // TODO(estade): handle non-tile drags.
237
238      this.cancelDelayedSwitch_();
239    },
240
241    /**
242     * The drag has left the navigation dot.
243     * @param {Event} e The MouseOver event for the drag.
244     * @private
245     */
246    doDragLeave: function(e) {
247      this.cancelDelayedSwitch_();
248    },
249
250    /**
251     * Cancels the timer for page switching.
252     * @private
253     */
254    cancelDelayedSwitch_: function() {
255      if (this.dragNavTimeout) {
256        window.clearTimeout(this.dragNavTimeout);
257        this.dragNavTimeout = null;
258      }
259    },
260
261    /**
262     * A transition has ended.
263     * @param {Event} e The transition end event.
264     * @private
265     */
266    onTransitionEnd_: function(e) {
267      if (e.propertyName === 'max-width' && this.classList.contains('small'))
268        this.parentNode.removeChild(this);
269    },
270  };
271
272  return {
273    NavDot: NavDot,
274  };
275});
276