blob: 5ae3e64bea5dab39151a4346b0fdc47c246dad9b [file] [log] [blame]
// Copyright 2014 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.
'use strict';
privateScriptController.installClass('HTMLMarqueeElement', function(HTMLMarqueeElementPrototype) {
var kDefaultScrollAmount = 6;
var kDefaultScrollDelayMS = 85;
var kMinimumScrollDelayMS = 60;
var kDefaultLoopLimit = -1;
var kBehaviorScroll = 'scroll';
var kBehaviorSlide = 'slide';
var kBehaviorAlternate = 'alternate';
var kDirectionLeft = 'left';
var kDirectionRight = 'right';
var kDirectionUp = 'up';
var kDirectionDown = 'down';
var kPresentationalAttributes = [
'bgcolor',
'height',
'hspace',
'vspace',
'width',
];
function convertHTMLLengthToCSSLength(value) {
var match = value.match(/^\s*([\d.]+)(%)?\s*/);
if (match)
return match[2] === '%' ? match[1] + '%' : match[1] + 'px';
return null;
}
// FIXME: Consider moving these utility functions to PrivateScriptRunner.js.
var kInt32Max = Math.pow(2, 31);
function convertToLong(n) {
// Using parseInt() is wrong but this aligns with the existing behavior of StringImpl::toInt().
// FIXME: Implement a correct algorithm of the Web IDL value conversion.
var value = parseInt(n);
if (!isNaN(value) && -kInt32Max <= value && value < kInt32Max)
return value;
return NaN;
}
function reflectAttribute(prototype, attributeName, propertyName) {
Object.defineProperty(prototype, propertyName, {
get: function() {
return this.getAttribute(attributeName) || '';
},
set: function(value) {
this.setAttribute(attributeName, value);
},
configurable: true,
enumerable: true,
});
}
function reflectBooleanAttribute(prototype, attributeName, propertyName) {
Object.defineProperty(prototype, propertyName, {
get: function() {
return this.getAttribute(attributeName);
},
set: function(value) {
if (value)
this.setAttribute(attributeName, '');
else
this.removeAttribute(attributeName);
},
});
}
reflectAttribute(HTMLMarqueeElementPrototype, 'behavior', 'behavior');
reflectAttribute(HTMLMarqueeElementPrototype, 'bgcolor', 'bgColor');
reflectAttribute(HTMLMarqueeElementPrototype, 'direction', 'direction');
reflectAttribute(HTMLMarqueeElementPrototype, 'height', 'height');
reflectAttribute(HTMLMarqueeElementPrototype, 'hspace', 'hspace');
reflectAttribute(HTMLMarqueeElementPrototype, 'vspace', 'vspace');
reflectAttribute(HTMLMarqueeElementPrototype, 'width', 'width');
reflectBooleanAttribute(HTMLMarqueeElementPrototype, 'truespeed', 'trueSpeed');
HTMLMarqueeElementPrototype.createdCallback = function() {
var shadow = this.createShadowRoot();
var style = document.createElement('style');
style.textContent = ':host { display: inline-block; width: -webkit-fill-available; overflow: hidden; text-align: initial; white-space: nowrap; }'
+ ':host([direction="up"]), :host([direction="down"]) { overflow: initial; overflow-y: hidden; white-space: initial; }';
shadow.appendChild(style);
var mover = document.createElement('div');
shadow.appendChild(mover);
mover.appendChild(document.createElement('content'));
this.loopCount_ = 0;
this.mover_ = mover;
this.player_ = null;
this.continueCallback_ = null;
};
HTMLMarqueeElementPrototype.attachedCallback = function() {
for (var i = 0; i < kPresentationalAttributes.length; ++i) {
this.initializeAttribute_(kPresentationalAttributes[i]);
}
this.start();
};
HTMLMarqueeElementPrototype.detachedCallback = function() {
this.stop();
};
HTMLMarqueeElementPrototype.attributeChangedCallback = function(name, oldValue, newValue) {
switch (name) {
case 'bgcolor':
this.style.backgroundColor = newValue;
break;
case 'height':
this.style.height = convertHTMLLengthToCSSLength(newValue);
break;
case 'hspace':
var margin = convertHTMLLengthToCSSLength(newValue);
this.style.marginLeft = margin;
this.style.marginRight = margin;
break;
case 'vspace':
var margin = convertHTMLLengthToCSSLength(newValue);
this.style.marginTop = margin;
this.style.marginBottom = margin;
break;
case 'width':
this.style.width = convertHTMLLengthToCSSLength(newValue);
break;
case 'behavior':
case 'direction':
case 'loop':
case 'scrollAmount':
case 'scrollDelay':
case 'trueSpeed':
// FIXME: Not implemented.
break;
}
};
HTMLMarqueeElementPrototype.initializeAttribute_ = function(name) {
var value = this.getAttribute(name);
if (value === null)
return;
this.attributeChangedCallback(name, null, value);
};
Object.defineProperty(HTMLMarqueeElementPrototype, 'scrollAmount', {
get: function() {
var value = this.getAttribute('scrollamount');
var scrollAmount = convertToLong(value);
if (isNaN(scrollAmount) || scrollAmount < 0)
return kDefaultScrollAmount;
return scrollAmount;
},
set: function(value) {
if (value < 0)
privateScriptController.throwException(privateScriptController.DOMException.IndexSizeError, "The provided value (" + value + ") is negative.");
this.setAttribute('scrollamount', value);
},
});
Object.defineProperty(HTMLMarqueeElementPrototype, 'scrollDelay', {
get: function() {
var value = this.getAttribute('scrolldelay');
var scrollDelay = convertToLong(value);
if (isNaN(scrollDelay) || scrollDelay < 0)
return kDefaultScrollDelayMS;
return scrollDelay;
},
set: function(value) {
if (value < 0)
privateScriptController.throwException(privateScriptController.DOMException.IndexSizeError, "The provided value (" + value + ") is negative.");
this.setAttribute('scrolldelay', value);
},
});
Object.defineProperty(HTMLMarqueeElementPrototype, 'loop', {
get: function() {
var value = this.getAttribute('loop');
var loop = convertToLong(value);
if (isNaN(loop) || loop <= 0)
return kDefaultLoopLimit;
return loop;
},
set: function(value) {
if (value <= 0 && value != -1)
privateScriptController.throwException(privateScriptController.DOMException.IndexSizeError, "The provided value (" + value + ") is neither positive nor -1.");
this.setAttribute('loop', value);
},
});
HTMLMarqueeElementPrototype.getGetMetrics_ = function() {
var direction = this.direction.toLowerCase();
if (direction === 'up' || direction === 'down')
this.mover_.style.height = '-webkit-max-content';
else
this.mover_.style.width = '-webkit-max-content';
var moverStyle = getComputedStyle(this.mover_);
var marqueeStyle = getComputedStyle(this);
var metrics = {};
metrics.contentWidth = parseInt(moverStyle.width);
metrics.contentHeight = parseInt(moverStyle.height);
metrics.marqueeWidth = parseInt(marqueeStyle.width);
metrics.marqueeHeight = parseInt(marqueeStyle.height);
if (direction === 'up' || direction === 'down')
this.mover_.style.height = '';
else
this.mover_.style.width = '';
return metrics;
};
HTMLMarqueeElementPrototype.getAnimationParameters_ = function() {
var metrics = this.getGetMetrics_();
var totalWidth = metrics.marqueeWidth + metrics.contentWidth;
var totalHeight = metrics.marqueeHeight + metrics.contentHeight;
var innerWidth = metrics.marqueeWidth - metrics.contentWidth;
var innerHeight = metrics.marqueeHeight - metrics.contentHeight;
var parameters = {};
var direction = this.direction.toLowerCase();
switch (this.behavior) {
case kBehaviorScroll:
default:
switch (direction) {
case kDirectionLeft:
default:
parameters.transformBegin = 'translateX(' + metrics.marqueeWidth + 'px)';
parameters.transformEnd = 'translateX(-' + metrics.contentWidth + 'px)';
parameters.distance = totalWidth;
break;
case kDirectionRight:
parameters.transformBegin = 'translateX(-' + metrics.contentWidth + 'px)';
parameters.transformEnd = 'translateX(' + metrics.marqueeWidth + 'px)';
parameters.distance = totalWidth;
break;
case kDirectionUp:
parameters.transformBegin = 'translateY(' + metrics.marqueeHeight + 'px)';
parameters.transformEnd = 'translateY(-' + metrics.contentHeight + 'px)';
parameters.distance = totalHeight;
break;
case kDirectionDown:
parameters.transformBegin = 'translateY(-' + metrics.contentHeight + 'px)';
parameters.transformEnd = 'translateY(' + metrics.marqueeHeight + 'px)';
parameters.distance = totalHeight;
break;
}
break;
case kBehaviorAlternate:
switch (direction) {
case kDirectionLeft:
default:
parameters.transformBegin = 'translateX(' + (innerWidth >= 0 ? innerWidth : 0) + 'px)';
parameters.transformEnd = 'translateX(' + (innerWidth >= 0 ? 0 : innerWidth) + 'px)';
parameters.distance = Math.abs(innerWidth);
break;
case kDirectionRight:
parameters.transformBegin = 'translateX(' + (innerWidth >= 0 ? 0 : innerWidth) + 'px)';
parameters.transformEnd = 'translateX(' + (innerWidth >= 0 ? innerWidth : 0) + 'px)';
parameters.distance = Math.abs(innerWidth);
break;
case kDirectionUp:
parameters.transformBegin = 'translateY(' + (innerHeight >= 0 ? innerHeight : 0) + 'px)';
parameters.transformEnd = 'translateY(' + (innerHeight >= 0 ? 0 : innerHeight) + 'px)';
parameters.distance = Math.abs(innerHeight);
break;
case kDirectionDown:
parameters.transformBegin = 'translateY(' + (innerHeight >= 0 ? 0 : innerHeight) + 'px)';
parameters.transformEnd = 'translateY(' + (innerHeight >= 0 ? innerHeight : 0) + 'px)';
parameters.distance = Math.abs(innerHeight);
break;
}
if (this.loopCount_ % 2) {
var transform = parameters.transformBegin;
parameters.transformBegin = parameters.transformEnd;
parameters.transformEnd = transform;
}
break;
case kBehaviorSlide:
switch (direction) {
case kDirectionLeft:
default:
parameters.transformBegin = 'translateX(' + metrics.marqueeWidth + 'px)';
parameters.transformEnd = 'translateX(0)';
parameters.distance = metrics.marqueeWidth;
break;
case kDirectionRight:
parameters.transformBegin = 'translateX(-' + metrics.contentWidth + 'px)';
parameters.transformEnd = 'translateX(' + innerWidth + 'px)';
parameters.distance = metrics.marqueeWidth;
break;
case kDirectionUp:
parameters.transformBegin = 'translateY(' + metrics.marqueeHeight + 'px)';
parameters.transformEnd = 'translateY(0)';
parameters.distance = metrics.marqueeHeight;
break;
case kDirectionDown:
parameters.transformBegin = 'translateY(-' + metrics.contentHeight + 'px)';
parameters.transformEnd = 'translateY(' + innerHeight + 'px)';
parameters.distance = metrics.marqueeHeight;
break;
}
break;
}
return parameters
};
function animationFinished_(event) {
var player = event.target;
var marquee = player.marquee_;
marquee.loopCount_++;
marquee.start();
};
HTMLMarqueeElementPrototype.shouldContinue_ = function() {
var loop = this.loop;
// By default, slide loops only once.
if (loop <= 0 && this.behavior === kBehaviorSlide)
loop = 1;
if (loop <= 0)
return true;
return this.loopCount_ < loop;
};
HTMLMarqueeElementPrototype.continue_ = function() {
if (!this.shouldContinue_()) {
return;
}
if (this.player_ && this.player_.playState === 'paused') {
this.player_.play();
return;
}
var parameters = this.getAnimationParameters_();
var scrollDelay = this.scrollDelay;
if (scrollDelay < kMinimumScrollDelayMS && !this.trueSpeed)
scrollDelay = kDefaultScrollDelayMS;
var player = this.mover_.animate([
{ transform: parameters.transformBegin },
{ transform: parameters.transformEnd },
], {
duration: this.scrollAmount == 0 ? 0 : parameters.distance * scrollDelay / this.scrollAmount,
fill: 'forwards',
});
player.marquee_ = this;
player.onfinish = animationFinished_;
this.player_ = player;
};
HTMLMarqueeElementPrototype.start = function() {
if (this.continueCallback_)
return;
this.continueCallback_ = requestAnimationFrame(function() {
this.continueCallback_ = null;
this.continue_();
}.bind(this));
};
HTMLMarqueeElementPrototype.stop = function() {
if (this.continueCallback_) {
cancelAnimationFrame(this.continueCallback_);
this.continueCallback_ = null;
return;
}
if (this.player_) {
this.player_.pause();
}
};
});