1 // Copyright 2014 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.
7 installClass('HTMLMarqueeElement', function(HTMLMarqueeElementPrototype) {
9 var kDefaultScrollAmount = 6;
10 var kDefaultScrollDelayMS = 85;
11 var kMinimumScrollDelayMS = 60;
13 var kDefaultLoopLimit = -1;
15 var kBehaviorScroll = 'scroll';
16 var kBehaviorSlide = 'slide';
17 var kBehaviorAlternate = 'alternate';
19 var kDirectionLeft = 'left';
20 var kDirectionRight = 'right';
21 var kDirectionUp = 'up';
22 var kDirectionDown = 'down';
24 var kPresentationalAttributes = [
32 var pixelLengthRegexp = /^\s*([\d.]+)\s*$/;
33 var percentageLengthRegexp = /^\s*([\d.]+)\s*%\s*$/;
35 function convertHTMLLengthToCSSLength(value) {
36 var pixelMatch = value.match(pixelLengthRegexp);
38 return pixelMatch[1] + 'px';
39 var percentageMatch = value.match(percentageLengthRegexp);
41 return percentageMatch[1] + '%';
45 // FIXME: Consider moving these utility functions to PrivateScriptRunner.js.
46 var kInt32Max = Math.pow(2, 31);
48 function convertToLong(n) {
49 // Using parseInt() is wrong but this aligns with the existing behavior of StringImpl::toInt().
50 // FIXME: Implement a correct algorithm of the Web IDL value conversion.
51 var value = parseInt(n);
52 if (!isNaN(value) && -kInt32Max <= value && value < kInt32Max)
57 function reflectAttribute(prototype, attributeName, propertyName) {
58 Object.defineProperty(prototype, propertyName, {
60 return this.getAttribute(attributeName) || '';
62 set: function(value) {
63 this.setAttribute(attributeName, value);
70 function reflectBooleanAttribute(prototype, attributeName, propertyName) {
71 Object.defineProperty(prototype, propertyName, {
73 return this.hasAttribute(attributeName);
75 set: function(value) {
77 this.setAttribute(attributeName, '');
79 this.removeAttribute(attributeName);
84 function defineInlineEventHandler(prototype, eventName) {
85 var propertyName = 'on' + eventName;
86 // FIXME: We should use symbols here instead.
87 var functionPropertyName = propertyName + 'Function_';
88 var eventHandlerPropertyName = propertyName + 'EventHandler_';
89 Object.defineProperty(prototype, propertyName, {
91 var func = this[functionPropertyName];
94 set: function(value) {
95 var oldEventHandler = this[eventHandlerPropertyName];
97 this.removeEventListener(eventName, oldEventHandler);
98 // Notice that we wrap |value| in an anonymous function so that the
99 // author can't call removeEventListener themselves to unregister the
100 // inline event handler.
101 var newEventHandler = value ? function() { value.apply(this, arguments) } : null;
103 this.addEventListener(eventName, newEventHandler);
104 this[functionPropertyName] = value;
105 this[eventHandlerPropertyName] = newEventHandler;
110 reflectAttribute(HTMLMarqueeElementPrototype, 'behavior', 'behavior');
111 reflectAttribute(HTMLMarqueeElementPrototype, 'bgcolor', 'bgColor');
112 reflectAttribute(HTMLMarqueeElementPrototype, 'direction', 'direction');
113 reflectAttribute(HTMLMarqueeElementPrototype, 'height', 'height');
114 reflectAttribute(HTMLMarqueeElementPrototype, 'hspace', 'hspace');
115 reflectAttribute(HTMLMarqueeElementPrototype, 'vspace', 'vspace');
116 reflectAttribute(HTMLMarqueeElementPrototype, 'width', 'width');
117 reflectBooleanAttribute(HTMLMarqueeElementPrototype, 'truespeed', 'trueSpeed');
119 defineInlineEventHandler(HTMLMarqueeElementPrototype, 'start');
120 defineInlineEventHandler(HTMLMarqueeElementPrototype, 'finish');
121 defineInlineEventHandler(HTMLMarqueeElementPrototype, 'bounce');
123 HTMLMarqueeElementPrototype.createdCallback = function() {
124 var shadow = this.createShadowRoot();
125 var style = document.createElement('style');
126 style.textContent = ':host { display: inline-block; width: -webkit-fill-available; overflow: hidden; text-align: initial; }' +
127 ':host([direction="up"]), :host([direction="down"]) { height: 200px; }';
128 shadow.appendChild(style);
130 var mover = document.createElement('div');
131 shadow.appendChild(mover);
133 mover.appendChild(document.createElement('content'));
138 this.continueCallback_ = null;
140 for (var i = 0; i < kPresentationalAttributes.length; ++i)
141 this.initializeAttribute_(kPresentationalAttributes[i]);
144 HTMLMarqueeElementPrototype.attachedCallback = function() {
148 HTMLMarqueeElementPrototype.detachedCallback = function() {
152 HTMLMarqueeElementPrototype.attributeChangedCallback = function(name, oldValue, newValue) {
155 this.style.backgroundColor = newValue;
158 this.style.height = convertHTMLLengthToCSSLength(newValue);
161 var margin = convertHTMLLengthToCSSLength(newValue);
162 this.style.marginLeft = margin;
163 this.style.marginRight = margin;
166 var margin = convertHTMLLengthToCSSLength(newValue);
167 this.style.marginTop = margin;
168 this.style.marginBottom = margin;
171 this.style.width = convertHTMLLengthToCSSLength(newValue);
182 HTMLMarqueeElementPrototype.initializeAttribute_ = function(name) {
183 var value = this.getAttribute(name);
186 this.attributeChangedCallback(name, null, value);
189 Object.defineProperty(HTMLMarqueeElementPrototype, 'scrollAmount', {
191 var value = this.getAttribute('scrollamount');
192 var scrollAmount = convertToLong(value);
193 if (isNaN(scrollAmount) || scrollAmount < 0)
194 return kDefaultScrollAmount;
197 set: function(value) {
199 throwException(PrivateScriptDOMException.IndexSizeError, "The provided value (" + value + ") is negative.");
200 this.setAttribute('scrollamount', value);
204 Object.defineProperty(HTMLMarqueeElementPrototype, 'scrollDelay', {
206 var value = this.getAttribute('scrolldelay');
207 var scrollDelay = convertToLong(value);
208 if (isNaN(scrollDelay) || scrollDelay < 0)
209 return kDefaultScrollDelayMS;
212 set: function(value) {
214 throwException(PrivateScriptDOMException.IndexSizeError, "The provided value (" + value + ") is negative.");
215 this.setAttribute('scrolldelay', value);
219 Object.defineProperty(HTMLMarqueeElementPrototype, 'loop', {
221 var value = this.getAttribute('loop');
222 var loop = convertToLong(value);
223 if (isNaN(loop) || loop <= 0)
224 return kDefaultLoopLimit;
227 set: function(value) {
228 if (value <= 0 && value != -1)
229 throwException(PrivateScriptDOMException.IndexSizeError, "The provided value (" + value + ") is neither positive nor -1.");
230 this.setAttribute('loop', value);
234 HTMLMarqueeElementPrototype.getGetMetrics_ = function() {
235 this.mover_.style.width = '-webkit-max-content';
237 var moverStyle = getComputedStyle(this.mover_);
238 var marqueeStyle = getComputedStyle(this);
241 metrics.contentWidth = parseInt(moverStyle.width);
242 metrics.contentHeight = parseInt(moverStyle.height);
243 metrics.marqueeWidth = parseInt(marqueeStyle.width);
244 metrics.marqueeHeight = parseInt(marqueeStyle.height);
246 this.mover_.style.width = '';
250 HTMLMarqueeElementPrototype.getAnimationParameters_ = function() {
251 var metrics = this.getGetMetrics_();
253 var totalWidth = metrics.marqueeWidth + metrics.contentWidth;
254 var totalHeight = metrics.marqueeHeight + metrics.contentHeight;
256 var innerWidth = metrics.marqueeWidth - metrics.contentWidth;
257 var innerHeight = metrics.marqueeHeight - metrics.contentHeight;
261 switch (this.behavior) {
262 case kBehaviorScroll:
264 switch (this.direction) {
267 parameters.transformBegin = 'translateX(' + metrics.marqueeWidth + 'px)';
268 parameters.transformEnd = 'translateX(-100%)';
269 parameters.distance = totalWidth;
271 case kDirectionRight:
272 parameters.transformBegin = 'translateX(-' + metrics.contentWidth + 'px)';
273 parameters.transformEnd = 'translateX(' + metrics.marqueeWidth + 'px)';
274 parameters.distance = totalWidth;
277 parameters.transformBegin = 'translateY(' + metrics.marqueeHeight + 'px)';
278 parameters.transformEnd = 'translateY(-' + metrics.contentHeight + 'px)';
279 parameters.distance = totalHeight;
282 parameters.transformBegin = 'translateY(-' + metrics.contentHeight + 'px)';
283 parameters.transformEnd = 'translateY(' + metrics.marqueeHeight + 'px)';
284 parameters.distance = totalHeight;
288 case kBehaviorAlternate:
289 switch (this.direction) {
292 parameters.transformBegin = 'translateX(' + innerWidth + 'px)';
293 parameters.transformEnd = 'translateX(0)';
294 parameters.distance = innerWidth;
296 case kDirectionRight:
297 parameters.transformBegin = 'translateX(0)';
298 parameters.transformEnd = 'translateX(' + innerWidth + 'px)';
299 parameters.distance = innerWidth;
302 parameters.transformBegin = 'translateY(' + innerHeight + 'px)';
303 parameters.transformEnd = 'translateY(0)';
304 parameters.distance = innerHeight;
307 parameters.transformBegin = 'translateY(0)';
308 parameters.transformEnd = 'translateY(' + innerHeight + 'px)';
309 parameters.distance = innerHeight;
313 if (this.loopCount_ % 2) {
314 var transform = parameters.transformBegin;
315 parameters.transformBegin = parameters.transformEnd;
316 parameters.transformEnd = transform;
321 switch (this.direction) {
324 parameters.transformBegin = 'translateX(' + metrics.marqueeWidth + 'px)';
325 parameters.transformEnd = 'translateX(0)';
326 parameters.distance = metrics.marqueeWidth;
328 case kDirectionRight:
329 parameters.transformBegin = 'translateX(-' + metrics.contentWidth + 'px)';
330 parameters.transformEnd = 'translateX(' + innerWidth + 'px)';
331 parameters.distance = metrics.marqueeWidth;
334 parameters.transformBegin = 'translateY(' + metrics.marqueeHeight + 'px)';
335 parameters.transformEnd = 'translateY(0)';
336 parameters.distance = metrics.marqueeHeight;
339 parameters.transformBegin = 'translateY(-' + metrics.contentHeight + 'px)';
340 parameters.transformEnd = 'translateY(' + innerHeight + 'px)';
341 parameters.distance = metrics.marqueeHeight;
350 HTMLMarqueeElementPrototype.shouldContinue_ = function() {
351 var loop = this.loop;
353 // By default, slide loops only once.
354 if (loop <= 0 && this.behavior === kBehaviorSlide)
359 return this.loopCount_ < loop;
362 HTMLMarqueeElementPrototype.continue_ = function() {
363 if (!this.shouldContinue_()) {
365 this.dispatchEvent(new Event('finish', false, true));
369 var parameters = this.getAnimationParameters_();
371 var player = this.mover_.animate([
372 { transform: parameters.transformBegin },
373 { transform: parameters.transformEnd },
375 duration: parameters.distance * this.scrollDelay / this.scrollAmount,
379 this.player_ = player;
381 player.addEventListener('finish', function() {
382 if (player != this.player_)
386 if (this.player_ && this.behavior === kBehaviorAlternate)
387 this.dispatchEvent(new Event('bounce', false, true));
391 HTMLMarqueeElementPrototype.start = function() {
392 if (this.continueCallback_ || this.player_)
394 this.continueCallback_ = requestAnimationFrame(function() {
395 this.continueCallback_ = null;
398 this.dispatchEvent(new Event('start', false, true));
401 HTMLMarqueeElementPrototype.stop = function() {
402 if (!this.continueCallback_ && !this.player_)
405 if (this.continueCallback_) {
406 cancelAnimationFrame(this.continueCallback_);
407 this.continueCallback_ = null;
411 // FIXME: Rather than canceling the animation, we really should just
412 // pause the animation, but the pause function is still flagged as
415 var player = this.player_;
421 // FIXME: We have to inject this HTMLMarqueeElement as a custom element in order to make
422 // createdCallback, attachedCallback, detachedCallback and attributeChangedCallback workable.
423 // document.registerElement('i-marquee', {
424 // prototype: HTMLMarqueeElementPrototype,