Home | History | Annotate | Download | only in html
      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.
      4 
      5 'use strict';
      6 
      7 installClass('HTMLMarqueeElement', function(HTMLMarqueeElementPrototype) {
      8 
      9     var kDefaultScrollAmount = 6;
     10     var kDefaultScrollDelayMS = 85;
     11     var kMinimumScrollDelayMS = 60;
     12 
     13     var kDefaultLoopLimit = -1;
     14 
     15     var kBehaviorScroll = 'scroll';
     16     var kBehaviorSlide = 'slide';
     17     var kBehaviorAlternate = 'alternate';
     18 
     19     var kDirectionLeft = 'left';
     20     var kDirectionRight = 'right';
     21     var kDirectionUp = 'up';
     22     var kDirectionDown = 'down';
     23 
     24     var kPresentationalAttributes = [
     25         'bgcolor',
     26         'height',
     27         'hspace',
     28         'vspace',
     29         'width',
     30     ];
     31 
     32     var pixelLengthRegexp = /^\s*([\d.]+)\s*$/;
     33     var percentageLengthRegexp = /^\s*([\d.]+)\s*%\s*$/;
     34 
     35     function convertHTMLLengthToCSSLength(value) {
     36         var pixelMatch = value.match(pixelLengthRegexp);
     37         if (pixelMatch)
     38             return pixelMatch[1] + 'px';
     39         var percentageMatch = value.match(percentageLengthRegexp);
     40         if (percentageMatch)
     41             return percentageMatch[1] + '%';
     42         return null;
     43     }
     44 
     45     // FIXME: Consider moving these utility functions to PrivateScriptRunner.js.
     46     var kInt32Max = Math.pow(2, 31);
     47 
     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)
     53             return value;
     54         return NaN;
     55     }
     56 
     57     function reflectAttribute(prototype, attributeName, propertyName) {
     58         Object.defineProperty(prototype, propertyName, {
     59             get: function() {
     60                 return this.getAttribute(attributeName) || '';
     61             },
     62             set: function(value) {
     63                 this.setAttribute(attributeName, value);
     64             },
     65             configurable: true,
     66             enumerable: true,
     67         });
     68     }
     69 
     70     function reflectBooleanAttribute(prototype, attributeName, propertyName) {
     71         Object.defineProperty(prototype, propertyName, {
     72             get: function() {
     73                 return this.hasAttribute(attributeName);
     74             },
     75             set: function(value) {
     76                 if (value)
     77                     this.setAttribute(attributeName, '');
     78                 else
     79                     this.removeAttribute(attributeName);
     80             },
     81         });
     82     }
     83 
     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, {
     90             get: function() {
     91                 var func = this[functionPropertyName];
     92                 return func || null;
     93             },
     94             set: function(value) {
     95                 var oldEventHandler = this[eventHandlerPropertyName];
     96                 if (oldEventHandler)
     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;
    102                 if (newEventHandler)
    103                     this.addEventListener(eventName, newEventHandler);
    104                 this[functionPropertyName] = value;
    105                 this[eventHandlerPropertyName] = newEventHandler;
    106             },
    107         });
    108     }
    109 
    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');
    118 
    119     defineInlineEventHandler(HTMLMarqueeElementPrototype, 'start');
    120     defineInlineEventHandler(HTMLMarqueeElementPrototype, 'finish');
    121     defineInlineEventHandler(HTMLMarqueeElementPrototype, 'bounce');
    122 
    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);
    129 
    130         var mover = document.createElement('div');
    131         shadow.appendChild(mover);
    132 
    133         mover.appendChild(document.createElement('content'));
    134 
    135         this.loopCount_ = 0;
    136         this.mover_ = mover;
    137         this.player_ = null;
    138         this.continueCallback_ = null;
    139 
    140         for (var i = 0; i < kPresentationalAttributes.length; ++i)
    141             this.initializeAttribute_(kPresentationalAttributes[i]);
    142     };
    143 
    144     HTMLMarqueeElementPrototype.attachedCallback = function() {
    145         this.start();
    146     };
    147 
    148     HTMLMarqueeElementPrototype.detachedCallback = function() {
    149         this.stop();
    150     };
    151 
    152     HTMLMarqueeElementPrototype.attributeChangedCallback = function(name, oldValue, newValue) {
    153         switch (name) {
    154         case 'bgcolor':
    155             this.style.backgroundColor = newValue;
    156             break;
    157         case 'height':
    158             this.style.height = convertHTMLLengthToCSSLength(newValue);
    159             break;
    160         case 'hspace':
    161             var margin = convertHTMLLengthToCSSLength(newValue);
    162             this.style.marginLeft = margin;
    163             this.style.marginRight = margin;
    164             break;
    165         case 'vspace':
    166             var margin = convertHTMLLengthToCSSLength(newValue);
    167             this.style.marginTop = margin;
    168             this.style.marginBottom = margin;
    169             break;
    170         case 'width':
    171             this.style.width = convertHTMLLengthToCSSLength(newValue);
    172             break;
    173         case 'behavior':
    174         case 'direction':
    175             this.stop();
    176             this.loopCount_ = 0;
    177             this.start();
    178             break;
    179         }
    180     };
    181 
    182     HTMLMarqueeElementPrototype.initializeAttribute_ = function(name) {
    183         var value = this.getAttribute(name);
    184         if (value === null)
    185             return;
    186         this.attributeChangedCallback(name, null, value);
    187     };
    188 
    189     Object.defineProperty(HTMLMarqueeElementPrototype, 'scrollAmount', {
    190         get: function() {
    191             var value = this.getAttribute('scrollamount');
    192             var scrollAmount = convertToLong(value);
    193             if (isNaN(scrollAmount) || scrollAmount < 0)
    194                 return kDefaultScrollAmount;
    195             return scrollAmount;
    196         },
    197         set: function(value) {
    198             if (value < 0)
    199                 throwException(PrivateScriptDOMException.IndexSizeError, "The provided value (" + value + ") is negative.");
    200             this.setAttribute('scrollamount', value);
    201         },
    202     });
    203 
    204     Object.defineProperty(HTMLMarqueeElementPrototype, 'scrollDelay', {
    205         get: function() {
    206             var value = this.getAttribute('scrolldelay');
    207             var scrollDelay = convertToLong(value);
    208             if (isNaN(scrollDelay) || scrollDelay < 0)
    209                 return kDefaultScrollDelayMS;
    210             return scrollDelay;
    211         },
    212         set: function(value) {
    213             if (value < 0)
    214                 throwException(PrivateScriptDOMException.IndexSizeError, "The provided value (" + value + ") is negative.");
    215             this.setAttribute('scrolldelay', value);
    216         },
    217     });
    218 
    219     Object.defineProperty(HTMLMarqueeElementPrototype, 'loop', {
    220         get: function() {
    221             var value = this.getAttribute('loop');
    222             var loop = convertToLong(value);
    223             if (isNaN(loop) || loop <= 0)
    224                 return kDefaultLoopLimit;
    225             return loop;
    226         },
    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);
    231         },
    232     });
    233 
    234     HTMLMarqueeElementPrototype.getGetMetrics_ = function() {
    235         this.mover_.style.width = '-webkit-max-content';
    236 
    237         var moverStyle = getComputedStyle(this.mover_);
    238         var marqueeStyle = getComputedStyle(this);
    239 
    240         var metrics = {};
    241         metrics.contentWidth = parseInt(moverStyle.width);
    242         metrics.contentHeight = parseInt(moverStyle.height);
    243         metrics.marqueeWidth = parseInt(marqueeStyle.width);
    244         metrics.marqueeHeight = parseInt(marqueeStyle.height);
    245 
    246         this.mover_.style.width = '';
    247         return metrics;
    248     };
    249 
    250     HTMLMarqueeElementPrototype.getAnimationParameters_ = function() {
    251         var metrics = this.getGetMetrics_();
    252 
    253         var totalWidth = metrics.marqueeWidth + metrics.contentWidth;
    254         var totalHeight = metrics.marqueeHeight + metrics.contentHeight;
    255 
    256         var innerWidth = metrics.marqueeWidth - metrics.contentWidth;
    257         var innerHeight = metrics.marqueeHeight - metrics.contentHeight;
    258 
    259         var parameters = {};
    260 
    261         switch (this.behavior) {
    262         case kBehaviorScroll:
    263         default:
    264             switch (this.direction) {
    265             case kDirectionLeft:
    266             default:
    267                 parameters.transformBegin = 'translateX(' + metrics.marqueeWidth + 'px)';
    268                 parameters.transformEnd = 'translateX(-100%)';
    269                 parameters.distance = totalWidth;
    270                 break;
    271             case kDirectionRight:
    272                 parameters.transformBegin = 'translateX(-' + metrics.contentWidth + 'px)';
    273                 parameters.transformEnd = 'translateX(' + metrics.marqueeWidth + 'px)';
    274                 parameters.distance = totalWidth;
    275                 break;
    276             case kDirectionUp:
    277                 parameters.transformBegin = 'translateY(' + metrics.marqueeHeight + 'px)';
    278                 parameters.transformEnd = 'translateY(-' + metrics.contentHeight + 'px)';
    279                 parameters.distance = totalHeight;
    280                 break;
    281             case kDirectionDown:
    282                 parameters.transformBegin = 'translateY(-' + metrics.contentHeight + 'px)';
    283                 parameters.transformEnd = 'translateY(' + metrics.marqueeHeight + 'px)';
    284                 parameters.distance = totalHeight;
    285                 break;
    286             }
    287             break;
    288         case kBehaviorAlternate:
    289             switch (this.direction) {
    290             case kDirectionLeft:
    291             default:
    292                 parameters.transformBegin = 'translateX(' + innerWidth + 'px)';
    293                 parameters.transformEnd = 'translateX(0)';
    294                 parameters.distance = innerWidth;
    295                 break;
    296             case kDirectionRight:
    297                 parameters.transformBegin = 'translateX(0)';
    298                 parameters.transformEnd = 'translateX(' + innerWidth + 'px)';
    299                 parameters.distance = innerWidth;
    300                 break;
    301             case kDirectionUp:
    302                 parameters.transformBegin = 'translateY(' + innerHeight + 'px)';
    303                 parameters.transformEnd = 'translateY(0)';
    304                 parameters.distance = innerHeight;
    305                 break;
    306             case kDirectionDown:
    307                 parameters.transformBegin = 'translateY(0)';
    308                 parameters.transformEnd = 'translateY(' + innerHeight + 'px)';
    309                 parameters.distance = innerHeight;
    310                 break;
    311             }
    312 
    313             if (this.loopCount_ % 2) {
    314                 var transform = parameters.transformBegin;
    315                 parameters.transformBegin = parameters.transformEnd;
    316                 parameters.transformEnd = transform;
    317             }
    318 
    319             break;
    320         case kBehaviorSlide:
    321             switch (this.direction) {
    322             case kDirectionLeft:
    323             default:
    324                 parameters.transformBegin = 'translateX(' + metrics.marqueeWidth + 'px)';
    325                 parameters.transformEnd = 'translateX(0)';
    326                 parameters.distance = metrics.marqueeWidth;
    327                 break;
    328             case kDirectionRight:
    329                 parameters.transformBegin = 'translateX(-' + metrics.contentWidth + 'px)';
    330                 parameters.transformEnd = 'translateX(' + innerWidth + 'px)';
    331                 parameters.distance = metrics.marqueeWidth;
    332                 break;
    333             case kDirectionUp:
    334                 parameters.transformBegin = 'translateY(' + metrics.marqueeHeight + 'px)';
    335                 parameters.transformEnd = 'translateY(0)';
    336                 parameters.distance = metrics.marqueeHeight;
    337                 break;
    338             case kDirectionDown:
    339                 parameters.transformBegin = 'translateY(-' + metrics.contentHeight + 'px)';
    340                 parameters.transformEnd = 'translateY(' + innerHeight + 'px)';
    341                 parameters.distance = metrics.marqueeHeight;
    342                 break;
    343             }
    344             break;
    345         }
    346 
    347         return parameters
    348     };
    349 
    350     HTMLMarqueeElementPrototype.shouldContinue_ = function() {
    351         var loop = this.loop;
    352 
    353         // By default, slide loops only once.
    354         if (loop <= 0 && this.behavior === kBehaviorSlide)
    355             loop = 1;
    356 
    357         if (loop <= 0)
    358             return true;
    359         return this.loopCount_ < loop;
    360     };
    361 
    362     HTMLMarqueeElementPrototype.continue_ = function() {
    363         if (!this.shouldContinue_()) {
    364             this.player_ = null;
    365             this.dispatchEvent(new Event('finish', false, true));
    366             return;
    367         }
    368 
    369         var parameters = this.getAnimationParameters_();
    370 
    371         var player = this.mover_.animate([
    372             { transform: parameters.transformBegin },
    373             { transform: parameters.transformEnd },
    374         ], {
    375             duration: parameters.distance * this.scrollDelay / this.scrollAmount,
    376             fill: 'forwards',
    377         });
    378 
    379         this.player_ = player;
    380 
    381         player.addEventListener('finish', function() {
    382             if (player != this.player_)
    383                 return;
    384             ++this.loopCount_;
    385             this.continue_();
    386             if (this.player_ && this.behavior === kBehaviorAlternate)
    387                 this.dispatchEvent(new Event('bounce', false, true));
    388         }.bind(this));
    389     };
    390 
    391     HTMLMarqueeElementPrototype.start = function() {
    392         if (this.continueCallback_ || this.player_)
    393             return;
    394         this.continueCallback_ = requestAnimationFrame(function() {
    395             this.continueCallback_ = null;
    396             this.continue_();
    397         }.bind(this));
    398         this.dispatchEvent(new Event('start', false, true));
    399     };
    400 
    401     HTMLMarqueeElementPrototype.stop = function() {
    402         if (!this.continueCallback_ && !this.player_)
    403             return;
    404 
    405         if (this.continueCallback_) {
    406             cancelAnimationFrame(this.continueCallback_);
    407             this.continueCallback_ = null;
    408             return;
    409         }
    410 
    411         // FIXME: Rather than canceling the animation, we really should just
    412         // pause the animation, but the pause function is still flagged as
    413         // experimental.
    414         if (this.player_) {
    415             var player = this.player_;
    416             this.player_ = null;
    417             player.cancel();
    418         }
    419     };
    420 
    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,
    425     // });
    426 });
    427