Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / first_run / step.js
blob213ea27c02e97919b48249f7274502df1b721ffd
1 // Copyright 2013 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.
5 /**
6  * Prototype for first-run tutorial steps.
7  */
9 cr.define('cr.FirstRun', function() {
10   var Step = cr.ui.define('div');
12   Step.prototype = {
13     __proto__: HTMLDivElement.prototype,
15     // Name of step.
16     name_: null,
18     // Button leading to next tutorial step.
19     nextButton_: null,
21     // Default control for this step.
22     defaultControl_: null,
24     decorate: function() {
25       this.name_ = this.getAttribute('id');
26       var controlsContainer = this.getElementsByClassName('controls')[0];
27       if (!controlsContainer)
28           throw Error('Controls not found.');
29       this.nextButton_ =
30           controlsContainer.getElementsByClassName('next-button')[0];
31       if (!this.nextButton_)
32         throw Error('Next button not found.');
33       this.nextButton_.addEventListener('click', (function(e) {
34         chrome.send('nextButtonClicked', [this.getName()]);
35         e.stopPropagation();
36       }).bind(this));
37       this.defaultControl_ = controlsContainer.children[0];
38     },
40     /**
41      * Returns name of the string.
42      */
43     getName: function() {
44       return this.name_;
45     },
47     /**
48      * Hides the step.
49      * @param {boolean} animated Whether transition should be animated.
50      * @param {function()=} opt_onHidden Called after step has been hidden.
51      */
52     hide: function(animated, opt_onHidden) {
53       var transitionDuration =
54           animated ? cr.FirstRun.getDefaultTransitionDuration() : 0;
55       changeVisibility(this,
56                        false,
57                        transitionDuration,
58                        function() {
59                          this.classList.add('hidden');
60                          if (opt_onHidden)
61                             opt_onHidden();
62                        }.bind(this));
63     },
65     /**
66      * Shows the step.
67      * @param {boolean} animated Whether transition should be animated.
68      * @param {function(Step)=} opt_onShown Called after step has been shown.
69      */
70     show: function(animated, opt_onShown) {
71       var transitionDuration =
72           animated ? cr.FirstRun.getDefaultTransitionDuration() : 0;
73       this.classList.remove('hidden');
74       changeVisibility(this,
75                        true,
76                        transitionDuration,
77                        function() {
78                          if (opt_onShown)
79                            opt_onShown(this);
80                        }.bind(this));
81     },
83     /**
84      * Sets position of the step.
85      * @param {object} position Parameter with optional fields |top|,
86      *     |right|, |bottom|, |left| holding corresponding offsets.
87      */
88     setPosition: function(position) {
89       var style = this.style;
90       ['top', 'right', 'bottom', 'left'].forEach(function(property) {
91         if (position.hasOwnProperty(property))
92           style.setProperty(property, position[property] + 'px');
93       });
94     },
96     /**
97      * Makes default control focused. Default control is a first control in
98      * current implementation.
99      */
100     focusDefaultControl: function() {
101       this.defaultControl_.focus();
102     },
103   };
105   var Bubble = cr.ui.define('div');
107   // List of rules declaring bubble's arrow position depending on text direction
108   // and shelf alignment. Every rule has required field |position| with list
109   // of classes that should be applied to arrow element if this rule choosen.
110   // The rule is suitable if its |shelf| and |dir| fields are correspond
111   // to current shelf alignment and text direction. Missing fields behaves like
112   // '*' wildcard. The last suitable rule in list is choosen for arrow style.
113   var ARROW_POSITION = {
114     'app-list': [
115       {
116         position: ['points-down', 'left']
117       },
118       {
119         dir: 'rtl',
120         position: ['points-down', 'right']
121       },
122       {
123         shelf: 'left',
124         position: ['points-left', 'top']
125       },
126       {
127         shelf: 'right',
128         position: ['points-right', 'top']
129       }
130     ],
131     'tray': [
132       {
133         position: ['points-right', 'top']
134       },
135       {
136         dir: 'rtl',
137         shelf: 'bottom',
138         position: ['points-left', 'top']
139       },
140       {
141         shelf: 'left',
142         position: ['points-left', 'top']
143       }
144     ],
145     'help': [
146       {
147         position: ['points-right', 'bottom']
148       },
149       {
150         dir: 'rtl',
151         shelf: 'bottom',
152         position: ['points-left', 'bottom']
153       },
154       {
155         shelf: 'left',
156         position: ['points-left', 'bottom']
157       }
158     ]
159   };
161   var DISTANCE_TO_POINTEE = 10;
162   var MINIMAL_SCREEN_OFFSET = 10;
163   var ARROW_LENGTH = 6; // Keep synced with .arrow border-width.
165   Bubble.prototype = {
166     __proto__: Step.prototype,
168     // Element displaying arrow.
169     arrow_: null,
171     // Unit vector directed along the bubble arrow.
172     direction_: null,
174     /**
175      * In addition to base class 'decorate' this method creates arrow and
176      * sets some properties related to arrow.
177      */
178     decorate: function() {
179       Step.prototype.decorate.call(this);
180       this.arrow_ = document.createElement('div');
181       this.arrow_.classList.add('arrow');
182       this.appendChild(this.arrow_);
183       var inputDirection = document.documentElement.getAttribute('dir');
184       var shelfAlignment = document.documentElement.getAttribute('shelf');
185       var isSuitable = function(rule) {
186         var inputDirectionMatch = !rule.hasOwnProperty('dir') ||
187                                   rule.dir === inputDirection;
188         var shelfAlignmentMatch = !rule.hasOwnProperty('shelf') ||
189                                   rule.shelf === shelfAlignment;
190         return inputDirectionMatch && shelfAlignmentMatch;
191       };
192       var lastSuitableRule = null;
193       var rules = ARROW_POSITION[this.getName()];
194       rules.forEach(function(rule) {
195         if (isSuitable(rule))
196           lastSuitableRule = rule;
197       });
198       assert(lastSuitableRule);
199       lastSuitableRule.position.forEach(function(cls) {
200         this.arrow_.classList.add(cls);
201       }.bind(this));
202       var list = this.arrow_.classList;
203       if (list.contains('points-up'))
204         this.direction_ = [0, -1];
205       else if (list.contains('points-right'))
206         this.direction_ = [1, 0];
207       else if (list.contains('points-down'))
208         this.direction_ = [0, 1];
209       else // list.contains('points-left')
210         this.direction_ = [-1, 0];
211     },
213     /**
214      * Sets position of bubble in such a maner that bubble's arrow points to
215      * given point.
216      * @param {Array} point Bubble arrow should point to this point after
217      *     positioning. |point| has format [x, y].
218      * @param {offset} number Additional offset from |point|.
219      */
220     setPointsTo: function(point, offset) {
221       var shouldShowBefore = this.hidden;
222       // "Showing" bubble in order to make offset* methods work.
223       if (shouldShowBefore) {
224         this.style.setProperty('opacity', '0');
225         this.show(false);
226       }
227       var arrow = [this.arrow_.offsetLeft + this.arrow_.offsetWidth / 2,
228                    this.arrow_.offsetTop + this.arrow_.offsetHeight / 2];
229       var totalOffset = DISTANCE_TO_POINTEE + offset;
230       var left = point[0] - totalOffset * this.direction_[0] - arrow[0];
231       var top = point[1] - totalOffset * this.direction_[1] - arrow[1];
232       // Force bubble to be inside screen.
233       if (this.arrow_.classList.contains('points-up') ||
234           this.arrow_.classList.contains('points-down')) {
235         left = Math.max(left, MINIMAL_SCREEN_OFFSET);
236         left = Math.min(left, document.body.offsetWidth - this.offsetWidth -
237             MINIMAL_SCREEN_OFFSET);
238       }
239       if (this.arrow_.classList.contains('points-left') ||
240           this.arrow_.classList.contains('points-right')) {
241         top = Math.max(top, MINIMAL_SCREEN_OFFSET);
242         top = Math.min(top, document.body.offsetHeight - this.offsetHeight -
243             MINIMAL_SCREEN_OFFSET);
244       }
245       this.style.setProperty('left', left + 'px');
246       this.style.setProperty('top', top + 'px');
247       if (shouldShowBefore) {
248         this.hide(false);
249         this.style.removeProperty('opacity');
250       }
251     },
253     /**
254      * Sets position of bubble. Overrides Step.setPosition to adjust offsets
255      * in case if its direction is the same as arrow's direction.
256      * @param {object} position Parameter with optional fields |top|,
257      *     |right|, |bottom|, |left| holding corresponding offsets.
258      */
259     setPosition: function(position) {
260       var arrow = this.arrow_;
261       // Increasing offset if it's from side where bubble points to.
262       [['top', 'points-up'],
263        ['right', 'points-right'],
264        ['bottom', 'points-down'],
265        ['left', 'points-left']].forEach(function(mapping) {
266           if (position.hasOwnProperty(mapping[0]) &&
267               arrow.classList.contains(mapping[1])) {
268             position[mapping[0]] += ARROW_LENGTH + DISTANCE_TO_POINTEE;
269           }
270         });
271       Step.prototype.setPosition.call(this, position);
272     },
273   };
275   var HelpStep = cr.ui.define('div');
277   HelpStep.prototype = {
278     __proto__: Bubble.prototype,
280     decorate: function() {
281       Bubble.prototype.decorate.call(this);
282       var helpButton = this.getElementsByClassName('help-button')[0];
283       helpButton.addEventListener('click', function(e) {
284         chrome.send('helpButtonClicked');
285         e.stopPropagation();
286       });
287     },
288   };
290   var DecorateStep = function(el) {
291     if (el.id == 'help')
292       HelpStep.decorate(el);
293     else if (el.classList.contains('bubble'))
294       Bubble.decorate(el);
295     else
296       Step.decorate(el);
297   };
299   return {DecorateStep: DecorateStep};