blob: f479959c1a967b5d749546c2a7b7778ffcf6f067 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Prototype for first-run tutorial steps.
*/
cr.define('cr.FirstRun', function() {
var Step = cr.ui.define('div');
Step.prototype = {
__proto__: HTMLDivElement.prototype,
// Name of step.
name_: null,
// Button leading to next tutorial step. For testing.
nextButton_: null,
// Default control for this step.
defaultControl_: null,
decorate: function() {
this.name_ = this.getAttribute('id');
var controlsContainer = this.getElementsByClassName('controls')[0];
if (!controlsContainer)
throw Error('Controls not found.');
var nextButtons = controlsContainer.getElementsByClassName('next-button');
if (nextButtons.length <= 0)
throw Error('Next button not found.');
this.nextButton_ = nextButtons[0];
Array.prototype.forEach.call(nextButtons, function(nextButton) {
nextButton.addEventListener(
'click', (function(e) {
chrome.send('nextButtonClicked', [this.getName()]);
e.stopPropagation();
}).bind(this));
}.bind(this));
this.defaultControl_ = controlsContainer.children[0];
},
/**
* Returns name of the string.
*/
getName: function() {
return this.name_;
},
/**
* Hides the step.
* @param {boolean} animated Whether transition should be animated.
* @param {function()=} opt_onHidden Called after step has been hidden.
*/
hide: function(animated, opt_onHidden) {
var transitionDuration =
animated ? cr.FirstRun.getDefaultTransitionDuration() : 0;
changeVisibility(this, false, transitionDuration, function() {
this.classList.add('hidden');
if (opt_onHidden)
opt_onHidden();
}.bind(this));
},
/**
* Shows the step.
* @param {boolean} animated Whether transition should be animated.
* @param {function(Step)=} opt_onShown Called after step has been shown.
*/
show: function(animated, opt_onShown) {
var transitionDuration =
animated ? cr.FirstRun.getDefaultTransitionDuration() : 0;
this.classList.remove('hidden');
changeVisibility(this, true, transitionDuration, function() {
if (opt_onShown)
opt_onShown(this);
}.bind(this));
},
/**
* Sets position of the step.
* @param {object} position Parameter with optional fields |top|,
* |right|, |bottom|, |left| holding corresponding offsets.
*/
setPosition: function(position) {
var style = this.style;
['top', 'right', 'bottom', 'left'].forEach(function(property) {
if (position.hasOwnProperty(property))
style.setProperty(property, position[property] + 'px');
});
},
/**
* Makes default control focused. Default control is a first control in
* current implementation.
*/
focusDefaultControl: function() {
this.defaultControl_.focus();
},
/**
* Updates UI when Google Assistant is enabled.
*/
setAssistantEnabled: function() {
if (this.name_ == 'app-list')
$('google-assistant-text').hidden = false;
},
};
var Bubble = cr.ui.define('div');
// List of rules declaring bubble's arrow position depending on text direction
// and shelf alignment. Every rule has required field |position| with list
// of classes that should be applied to arrow element if this rule choosen.
// The rule is suitable if its |shelf| and |dir| fields are correspond
// to current shelf alignment and text direction. Missing fields behaves like
// '*' wildcard. The last suitable rule in list is choosen for arrow style.
var ARROW_POSITION = {
'app-list': [
{position: ['points-down', 'left']},
{dir: 'rtl', position: ['points-down', 'right']},
{shelf: 'left', position: ['points-left', 'top']},
{shelf: 'right', position: ['points-right', 'top']}
],
'tray': [
{position: ['points-right', 'top']},
{dir: 'rtl', shelf: 'bottom', position: ['points-left', 'top']},
{shelf: 'left', position: ['points-left', 'top']}
],
'help': [
{position: ['points-right', 'bottom']},
{dir: 'rtl', shelf: 'bottom', position: ['points-left', 'bottom']},
{shelf: 'left', position: ['points-left', 'bottom']}
]
};
var DISTANCE_TO_POINTEE = 10;
var MINIMAL_SCREEN_OFFSET = 10;
var ARROW_LENGTH = 6; // Keep synced with .arrow border-width.
Bubble.prototype = {
__proto__: Step.prototype,
// Element displaying arrow.
arrow_: null,
// Unit vector directed along the bubble arrow.
direction_: null,
/**
* In addition to base class 'decorate' this method creates arrow and
* sets some properties related to arrow.
*/
decorate: function() {
Step.prototype.decorate.call(this);
this.arrow_ = document.createElement('div');
this.arrow_.classList.add('arrow');
this.appendChild(this.arrow_);
var inputDirection = document.documentElement.getAttribute('dir');
var shelfAlignment = document.documentElement.getAttribute('shelf');
var isSuitable = function(rule) {
var inputDirectionMatch =
!rule.hasOwnProperty('dir') || rule.dir === inputDirection;
var shelfAlignmentMatch =
!rule.hasOwnProperty('shelf') || rule.shelf === shelfAlignment;
return inputDirectionMatch && shelfAlignmentMatch;
};
var lastSuitableRule = null;
var rules = ARROW_POSITION[this.getName()];
rules.forEach(function(rule) {
if (isSuitable(rule))
lastSuitableRule = rule;
});
assert(lastSuitableRule);
lastSuitableRule.position.forEach(function(cls) {
this.arrow_.classList.add(cls);
}.bind(this));
var list = this.arrow_.classList;
if (list.contains('points-up'))
this.direction_ = [0, -1];
else if (list.contains('points-right'))
this.direction_ = [1, 0];
else if (list.contains('points-down'))
this.direction_ = [0, 1];
else // list.contains('points-left')
this.direction_ = [-1, 0];
},
/**
* Sets position of bubble in such a maner that bubble's arrow points to
* given point.
* @param {Array} point Bubble arrow should point to this point after
* positioning. |point| has format [x, y].
* @param {offset} number Additional offset from |point|.
*/
setPointsTo: function(point, offset) {
var shouldShowBefore = this.hidden;
// "Showing" bubble in order to make offset* methods work.
if (shouldShowBefore) {
this.style.setProperty('opacity', '0');
this.show(false);
}
var arrow = [
this.arrow_.offsetLeft + this.arrow_.offsetWidth / 2,
this.arrow_.offsetTop + this.arrow_.offsetHeight / 2
];
var totalOffset = DISTANCE_TO_POINTEE + offset;
var left = point[0] - totalOffset * this.direction_[0] - arrow[0];
var top = point[1] - totalOffset * this.direction_[1] - arrow[1];
// Force bubble to be inside screen.
if (this.arrow_.classList.contains('points-up') ||
this.arrow_.classList.contains('points-down')) {
left = Math.max(left, MINIMAL_SCREEN_OFFSET);
left = Math.min(
left,
document.body.offsetWidth - this.offsetWidth -
MINIMAL_SCREEN_OFFSET);
}
if (this.arrow_.classList.contains('points-left') ||
this.arrow_.classList.contains('points-right')) {
top = Math.max(top, MINIMAL_SCREEN_OFFSET);
top = Math.min(
top,
document.body.offsetHeight - this.offsetHeight -
MINIMAL_SCREEN_OFFSET);
}
this.style.setProperty('left', left + 'px');
this.style.setProperty('top', top + 'px');
if (shouldShowBefore) {
this.hide(false);
this.style.removeProperty('opacity');
}
},
/**
* Sets position of bubble. Overrides Step.setPosition to adjust offsets
* in case if its direction is the same as arrow's direction.
* @param {object} position Parameter with optional fields |top|,
* |right|, |bottom|, |left| holding corresponding offsets.
*/
setPosition: function(position) {
var arrow = this.arrow_;
// Increasing offset if it's from side where bubble points to.
[['top', 'points-up'], ['right', 'points-right'],
['bottom', 'points-down'], ['left', 'points-left']]
.forEach(function(mapping) {
if (position.hasOwnProperty(mapping[0]) &&
arrow.classList.contains(mapping[1])) {
position[mapping[0]] += ARROW_LENGTH + DISTANCE_TO_POINTEE;
}
});
Step.prototype.setPosition.call(this, position);
},
};
var TrayStep = cr.ui.define('div');
TrayStep.prototype = {
__proto__: Bubble.prototype,
decorate: function() {
Bubble.prototype.decorate.call(this);
var helpButton = this.getElementsByClassName('help-button')[0];
helpButton.addEventListener('click', function(e) {
chrome.send('helpButtonClicked');
e.stopPropagation();
});
},
};
var DecorateStep = function(el) {
if (el.id == 'tray')
TrayStep.decorate(el);
else if (el.classList.contains('bubble'))
Bubble.decorate(el);
else
Step.decorate(el);
};
return {DecorateStep: DecorateStep};
});