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