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. 4 5 /** 6 * Prototype for first-run tutorial steps. 7 */ 8 9 cr.define('cr.FirstRun', function() { 10 var Step = cr.ui.define('div'); 11 12 Step.prototype = { 13 __proto__: HTMLDivElement.prototype, 14 15 // Name of step. 16 name_: null, 17 18 // Button leading to next tutorial step. 19 nextButton_: null, 20 21 // Default control for this step. 22 defaultControl_: null, 23 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 }, 39 40 /** 41 * Returns name of the string. 42 */ 43 getName: function() { 44 return this.name_; 45 }, 46 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 }, 64 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 }, 82 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 }, 95 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 }; 104 105 var Bubble = cr.ui.define('div'); 106 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 }; 160 161 var DISTANCE_TO_POINTEE = 10; 162 var MINIMAL_SCREEN_OFFSET = 10; 163 var ARROW_LENGTH = 6; // Keep synced with .arrow border-width. 164 165 Bubble.prototype = { 166 __proto__: Step.prototype, 167 168 // Element displaying arrow. 169 arrow_: null, 170 171 // Unit vector directed along the bubble arrow. 172 direction_: null, 173 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 }, 212 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 }, 252 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 }; 274 275 var HelpStep = cr.ui.define('div'); 276 277 HelpStep.prototype = { 278 __proto__: Bubble.prototype, 279 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 }; 289 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 }; 298 299 return {DecorateStep: DecorateStep}; 300 }); 301