| // 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(); |
| } |
| }; |
| }); |