Implemented UI for new first-run tutorial.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / first_run / step.js
blobccfcb786f168163f9c2668660833e31fb5e9d283
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     // Whether screen is shown.
22     isShown_: false,
24     decorate: function() {
25       this.name_ = this.getAttribute('id');
26       this.nextButton_ = this.getElementsByClassName('next-button')[0];
27       if (!this.nextButton_)
28         throw Error('Next button not found.');
29       this.nextButton_.addEventListener('click', (function(e) {
30         chrome.send('nextButtonClicked', [this.getName()]);
31         e.stopPropagation();
32       }).bind(this));
33       // Make close unfocusable by mouse.
34       var topBar = this.getElementsByClassName('topbutton-bar')[0];
35       if (topBar) {
36         var closeButton = topBar.getElementsByClassName('close-button')[0];
37         if (closeButton) {
38           closeButton.addEventListener('mousedown', function(event) {
39             event.preventDefault();
40           });
41         }
42       }
43     },
45     /**
46      * Returns name of the string.
47      */
48     getName: function() {
49       return this.name_;
50     },
52     /**
53      * Hides the step.
54      */
55     hide: function() {
56       this.style.setProperty('display', 'none');
57       this.isShown_ = false;
58     },
60     /**
61      * Shows the step.
62      */
63     show: function() {
64       this.style.setProperty('display', 'inline-block');
65       this.isShown_ = true;
66     },
68     /**
69      * Sets position of the step.
70      * @param {object} position Parameter with optional fields |top|,
71      *     |right|, |bottom|, |left| holding corresponding offsets.
72      */
73     setPosition: function(position) {
74       var style = this.style;
75       ['top', 'right', 'bottom', 'left'].forEach(function(property) {
76         if (position.hasOwnProperty(property))
77           style.setProperty(property, position[property] + 'px');
78       });
79     }
80   };
82   var Bubble = cr.ui.define('div');
84   // Styles of .step which are used for arrow styling.
85   var ARROW_STYLES = [
86     'points-up',
87     'points-left',
88     'points-down',
89     'points-right',
90     'top',
91     'left',
92     'bottom',
93     'right'
94   ];
96   var DISTANCE_TO_POINTEE = 10;
97   var MINIMAL_SCREEN_OFFSET = 10;
98   var ARROW_LENGTH = 6; // Keep synced with .arrow border-width.
100   Bubble.prototype = {
101     __proto__: Step.prototype,
103     // Element displaying arrow.
104     arrow_: null,
106     // Unit vector directed along the bubble arrow.
107     direction_: null,
109     /**
110      * In addition to base class 'decorate' this method creates arrow and
111      * sets some properties related to arrow.
112      */
113     decorate: function() {
114       Step.prototype.decorate.call(this);
115       this.arrow_ = document.createElement('div');
116       this.arrow_.classList.add('arrow');
117       this.appendChild(this.arrow_);
118       ARROW_STYLES.forEach(function(style) {
119         if (!this.classList.contains(style))
120           return;
121         // Changing right to left in RTL case.
122         if (document.documentElement.getAttribute('dir') == 'rtl') {
123           style = style.replace(/right|left/, function(match) {
124             return (match == 'right') ? 'left' : 'right';
125           });
126         }
127         this.arrow_.classList.add(style);
128       }.bind(this));
129       var list = this.arrow_.classList;
130       if (list.contains('points-up'))
131         this.direction_ = [0, -1];
132       else if (list.contains('points-right'))
133         this.direction_ = [1, 0];
134       else if (list.contains('points-down'))
135         this.direction_ = [0, 1];
136       else // list.contains('points-left')
137         this.direction_ = [-1, 0];
138     },
140     /**
141      * Sets position of bubble in such a maner that bubble's arrow points to
142      * given point.
143      * @param {Array} point Bubble arrow should point to this point after
144      *     positioning. |point| has format [x, y].
145      * @param {offset} number Additional offset from |point|.
146      */
147     setPointsTo: function(point, offset) {
148       var shouldShowBefore = !this.isShown_;
149       // "Showing" bubble in order to make offset* methods work.
150       if (shouldShowBefore) {
151         this.style.setProperty('opacity', '0');
152         this.show();
153       }
154       var arrow = [this.arrow_.offsetLeft + this.arrow_.offsetWidth / 2,
155                    this.arrow_.offsetTop + this.arrow_.offsetHeight / 2];
156       var totalOffset = DISTANCE_TO_POINTEE + offset;
157       var left = point[0] - totalOffset * this.direction_[0] - arrow[0];
158       var top = point[1] - totalOffset * this.direction_[1] - arrow[1];
159       // Force bubble to be inside screen.
160       if (this.arrow_.classList.contains('points-up') ||
161           this.arrow_.classList.contains('points-down')) {
162         left = Math.max(left, MINIMAL_SCREEN_OFFSET);
163         left = Math.min(left, document.body.offsetWidth - this.offsetWidth -
164             MINIMAL_SCREEN_OFFSET);
165       }
166       if (this.arrow_.classList.contains('points-left') ||
167           this.arrow_.classList.contains('points-right')) {
168         top = Math.max(top, MINIMAL_SCREEN_OFFSET);
169         top = Math.min(top, document.body.offsetHeight - this.offsetHeight -
170             MINIMAL_SCREEN_OFFSET);
171       }
172       this.style.setProperty('left', left + 'px');
173       this.style.setProperty('top', top + 'px');
174       if (shouldShowBefore) {
175         this.style.setProperty('opacity', '1');
176         this.hide();
177       }
178     },
180     /**
181      * Sets position of bubble. Overrides Step.setPosition to adjust offsets
182      * in case if its direction is the same as arrow's direction.
183      * @param {object} position Parameter with optional fields |top|,
184      *     |right|, |bottom|, |left| holding corresponding offsets.
185      */
186     setPosition: function(position) {
187       var arrow = this.arrow_;
188       // Increasing offset if it's from side where bubble points to.
189       [['top', 'points-up'],
190        ['right', 'points-right'],
191        ['bottom', 'points-down'],
192        ['left', 'points-left']].forEach(function(mapping) {
193           if (position.hasOwnProperty(mapping[0]) &&
194               arrow.classList.contains(mapping[1])) {
195             position[mapping[0]] += ARROW_LENGTH + DISTANCE_TO_POINTEE;
196           }
197         });
198       Step.prototype.setPosition.call(this, position);
199     }
200   };
202   var DecorateStep = function(el) {
203     if (el.classList.contains('bubble'))
204       Bubble.decorate(el);
205     else
206       Step.decorate(el);
207   };
209   return {DecorateStep: DecorateStep};