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.
6 * Prototype for first-run tutorial steps.
9 cr.define('cr.FirstRun', function() {
10 var Step = cr.ui.define('div');
13 __proto__: HTMLDivElement.prototype,
18 // Button leading to next tutorial step.
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.');
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()]);
37 this.defaultControl_ = controlsContainer.children[0];
41 * Returns name of the string.
49 * @param {boolean} animated Whether transition should be animated.
50 * @param {function()=} opt_onHidden Called after step has been hidden.
52 hide: function(animated, opt_onHidden) {
53 var transitionDuration =
54 animated ? cr.FirstRun.getDefaultTransitionDuration() : 0;
55 changeVisibility(this,
59 this.classList.add('hidden');
67 * @param {boolean} animated Whether transition should be animated.
68 * @param {function(Step)=} opt_onShown Called after step has been shown.
70 show: function(animated, opt_onShown) {
71 var transitionDuration =
72 animated ? cr.FirstRun.getDefaultTransitionDuration() : 0;
73 this.classList.remove('hidden');
74 changeVisibility(this,
84 * Sets position of the step.
85 * @param {object} position Parameter with optional fields |top|,
86 * |right|, |bottom|, |left| holding corresponding offsets.
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');
97 * Makes default control focused. Default control is a first control in
98 * current implementation.
100 focusDefaultControl: function() {
101 this.defaultControl_.focus();
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 = {
116 position: ['points-down', 'left']
120 position: ['points-down', 'right']
124 position: ['points-left', 'top']
128 position: ['points-right', 'top']
133 position: ['points-right', 'top']
138 position: ['points-left', 'top']
142 position: ['points-left', 'top']
147 position: ['points-right', 'bottom']
152 position: ['points-left', 'bottom']
156 position: ['points-left', 'bottom']
161 var DISTANCE_TO_POINTEE = 10;
162 var MINIMAL_SCREEN_OFFSET = 10;
163 var ARROW_LENGTH = 6; // Keep synced with .arrow border-width.
166 __proto__: Step.prototype,
168 // Element displaying arrow.
171 // Unit vector directed along the bubble arrow.
175 * In addition to base class 'decorate' this method creates arrow and
176 * sets some properties related to arrow.
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;
192 var lastSuitableRule = null;
193 var rules = ARROW_POSITION[this.getName()];
194 rules.forEach(function(rule) {
195 if (isSuitable(rule))
196 lastSuitableRule = rule;
198 assert(lastSuitableRule);
199 lastSuitableRule.position.forEach(function(cls) {
200 this.arrow_.classList.add(cls);
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];
214 * Sets position of bubble in such a maner that bubble's arrow points to
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|.
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');
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);
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);
245 this.style.setProperty('left', left + 'px');
246 this.style.setProperty('top', top + 'px');
247 if (shouldShowBefore) {
249 this.style.removeProperty('opacity');
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.
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;
271 Step.prototype.setPosition.call(this, position);
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');
290 var DecorateStep = function(el) {
292 HelpStep.decorate(el);
293 else if (el.classList.contains('bubble'))
299 return {DecorateStep: DecorateStep};