Home | History | Annotate | Download | only in first_run
      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