├── README.md ├── test.html ├── LICENSE └── marquee.js /README.md: -------------------------------------------------------------------------------- 1 | marquee 2 | ======= 3 | 4 | An implementation of <marquee> using web components 5 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 4 | 22 | 23 | 24 | 25 |
26 | Hello, world 27 |
28 | 29 |
30 | Hello, world 31 |
32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Adam Barth 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /marquee.js: -------------------------------------------------------------------------------- 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 | (function(global) { 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 | function reflectAttribute(prototype, attributeName, propertyName) { 46 | Object.defineProperty(prototype, propertyName, { 47 | get: function() { 48 | return this.getAttribute(attributeName) || ''; 49 | }, 50 | set: function(value) { 51 | this.setAttribute(attributeName, value); 52 | }, 53 | }); 54 | } 55 | 56 | function reflectBooleanAttribute(prototype, attributeName, propertyName) { 57 | Object.defineProperty(prototype, propertyName, { 58 | get: function() { 59 | return this.hasAttribute(attributeName); 60 | }, 61 | set: function(value) { 62 | this.setAttribute(attributeName, value ? '' : null); 63 | }, 64 | }); 65 | } 66 | 67 | function defineInlineEventHandler(prototype, eventName) { 68 | var propertyName = 'on' + eventName; 69 | // TODO(abarth): We should use symbols here instead. 70 | var functionPropertyName = propertyName + 'Function_'; 71 | var eventHandlerPropertyName = propertyName + 'EventHandler_'; 72 | Object.defineProperty(prototype, propertyName, { 73 | get: function() { 74 | var func = this[functionPropertyName]; 75 | return func || null; 76 | }, 77 | set: function(value) { 78 | var oldEventHandler = this[eventHandlerPropertyName]; 79 | if (oldEventHandler) 80 | this.removeEventListener(eventName, oldEventHandler); 81 | // Notice that we wrap |value| in an anonymous function so that the 82 | // author can't call removeEventListener themselves to unregister the 83 | // inline event handler. 84 | var newEventHandler = value ? function() { value.apply(this, arguments) } : null; 85 | if (newEventHandler) 86 | this.addEventListener(eventName, newEventHandler); 87 | this[functionPropertyName] = value; 88 | this[eventHandlerPropertyName] = newEventHandler; 89 | }, 90 | }); 91 | } 92 | 93 | var HTMLMarqueeElementPrototype = Object.create(HTMLElement.prototype); 94 | 95 | reflectAttribute(HTMLMarqueeElementPrototype, 'behavior', 'behavior'); 96 | reflectAttribute(HTMLMarqueeElementPrototype, 'bgcolor', 'bgColor'); 97 | reflectAttribute(HTMLMarqueeElementPrototype, 'direction', 'direction'); 98 | reflectAttribute(HTMLMarqueeElementPrototype, 'height', 'height'); 99 | reflectAttribute(HTMLMarqueeElementPrototype, 'hspace', 'hspace'); 100 | reflectAttribute(HTMLMarqueeElementPrototype, 'vspace', 'vspace'); 101 | reflectAttribute(HTMLMarqueeElementPrototype, 'width', 'width'); 102 | reflectBooleanAttribute(HTMLMarqueeElementPrototype, 'truespeed', 'trueSpeed'); 103 | 104 | defineInlineEventHandler(HTMLMarqueeElementPrototype, 'start'); 105 | defineInlineEventHandler(HTMLMarqueeElementPrototype, 'finish'); 106 | defineInlineEventHandler(HTMLMarqueeElementPrototype, 'bounce'); 107 | 108 | HTMLMarqueeElementPrototype.createdCallback = function() { 109 | var shadow = this.createShadowRoot(); 110 | var style = global.document.createElement('style'); 111 | style.textContent = ':host { display: inline-block; width: -webkit-fill-available; overflow: hidden; text-align: initial; }' + 112 | ':host([direction="up"]), :host([direction="down"]) { height: 200px; }'; 113 | shadow.appendChild(style); 114 | 115 | var mover = global.document.createElement('div'); 116 | shadow.appendChild(mover); 117 | 118 | mover.appendChild(global.document.createElement('content')); 119 | 120 | this.loopCount_ = 0; 121 | this.mover_ = mover; 122 | this.player_ = null; 123 | this.continueCallback_ = null; 124 | 125 | for (var i = 0; i < kPresentationalAttributes.length; ++i) 126 | this.initializeAttribute_(kPresentationalAttributes[i]); 127 | }; 128 | 129 | HTMLMarqueeElementPrototype.attachedCallback = function() { 130 | this.start(); 131 | }; 132 | 133 | HTMLMarqueeElementPrototype.detachedCallback = function() { 134 | this.stop(); 135 | }; 136 | 137 | HTMLMarqueeElementPrototype.attributeChangedCallback = function(name, oldValue, newValue) { 138 | switch (name) { 139 | case 'bgcolor': 140 | this.style.backgroundColor = newValue; 141 | break; 142 | case 'height': 143 | this.style.height = convertHTMLLengthToCSSLength(newValue); 144 | break; 145 | case 'hspace': 146 | var margin = convertHTMLLengthToCSSLength(newValue); 147 | this.style.marginLeft = margin; 148 | this.style.marginRight = margin; 149 | break; 150 | case 'vspace': 151 | var margin = convertHTMLLengthToCSSLength(newValue); 152 | this.style.marginTop = margin; 153 | this.style.marginBottom = margin; 154 | break; 155 | case 'width': 156 | this.style.width = convertHTMLLengthToCSSLength(newValue); 157 | break; 158 | case 'behavior': 159 | case 'direction': 160 | this.stop(); 161 | this.loopCount_ = 0; 162 | this.start(); 163 | break; 164 | } 165 | }; 166 | 167 | HTMLMarqueeElementPrototype.initializeAttribute_ = function(name) { 168 | var value = this.getAttribute(name); 169 | if (value === null) 170 | return; 171 | this.attributeChangedCallback(name, null, value); 172 | }; 173 | 174 | Object.defineProperty(HTMLMarqueeElementPrototype, 'scrollAmount', { 175 | get: function() { 176 | var value = this.getAttribute('scrollamount'); 177 | return value === null ? kDefaultScrollAmount : parseInt(value); 178 | }, 179 | set: function(value) { 180 | this.setAttribute('scrollamount', +value); 181 | }, 182 | }); 183 | 184 | Object.defineProperty(HTMLMarqueeElementPrototype, 'scrollDelay', { 185 | get: function() { 186 | var value = this.getAttribute('scrolldelay'); 187 | if (value === null) 188 | return kDefaultScrollDelayMS; 189 | var scrollDelay = parseInt(value); 190 | if (scrollDelay < kMinimumScrollDelayMS && !this.trueSpeed) 191 | return kDefaultScrollDelayMS; 192 | return scrollDelay; 193 | }, 194 | set: function(value) { 195 | this.setAttribute('scrolldelay', +value); 196 | }, 197 | }); 198 | 199 | Object.defineProperty(HTMLMarqueeElementPrototype, 'loop', { 200 | get: function() { 201 | var value = this.getAttribute('loop'); 202 | return value === null ? kDefaultLoopLimit : parseInt(value); 203 | }, 204 | set: function(value) { 205 | this.setAttribute('loop', +value); 206 | }, 207 | }); 208 | 209 | HTMLMarqueeElementPrototype.getGetMetrics_ = function() { 210 | this.mover_.style.width = '-webkit-max-content'; 211 | 212 | var moverStyle = global.getComputedStyle(this.mover_); 213 | var marqueeStyle = global.getComputedStyle(this); 214 | 215 | var metrics = {}; 216 | metrics.contentWidth = parseInt(moverStyle.width); 217 | metrics.contentHeight = parseInt(moverStyle.height); 218 | metrics.marqueeWidth = parseInt(marqueeStyle.width); 219 | metrics.marqueeHeight = parseInt(marqueeStyle.height); 220 | 221 | this.mover_.style.width = ''; 222 | return metrics; 223 | }; 224 | 225 | HTMLMarqueeElementPrototype.getAnimationParameters_ = function() { 226 | var metrics = this.getGetMetrics_(); 227 | 228 | var totalWidth = metrics.marqueeWidth + metrics.contentWidth; 229 | var totalHeight = metrics.marqueeHeight + metrics.contentHeight; 230 | 231 | var innerWidth = metrics.marqueeWidth - metrics.contentWidth; 232 | var innerHeight = metrics.marqueeHeight - metrics.contentHeight; 233 | 234 | var parameters = {}; 235 | 236 | switch (this.behavior) { 237 | case kBehaviorScroll: 238 | default: 239 | switch (this.direction) { 240 | case kDirectionLeft: 241 | default: 242 | parameters.transformBegin = 'translateX(' + metrics.marqueeWidth + 'px)'; 243 | parameters.transformEnd = 'translateX(-100%)'; 244 | parameters.distance = totalWidth; 245 | break; 246 | case kDirectionRight: 247 | parameters.transformBegin = 'translateX(-' + metrics.contentWidth + 'px)'; 248 | parameters.transformEnd = 'translateX(' + metrics.marqueeWidth + 'px)'; 249 | parameters.distance = totalWidth; 250 | break; 251 | case kDirectionUp: 252 | parameters.transformBegin = 'translateY(' + metrics.marqueeHeight + 'px)'; 253 | parameters.transformEnd = 'translateY(-' + metrics.contentHeight + 'px)'; 254 | parameters.distance = totalHeight; 255 | break; 256 | case kDirectionDown: 257 | parameters.transformBegin = 'translateY(-' + metrics.contentHeight + 'px)'; 258 | parameters.transformEnd = 'translateY(' + metrics.marqueeHeight + 'px)'; 259 | parameters.distance = totalHeight; 260 | break; 261 | } 262 | break; 263 | case kBehaviorAlternate: 264 | switch (this.direction) { 265 | case kDirectionLeft: 266 | default: 267 | parameters.transformBegin = 'translateX(' + innerWidth + 'px)'; 268 | parameters.transformEnd = 'translateX(0)'; 269 | parameters.distance = innerWidth; 270 | break; 271 | case kDirectionRight: 272 | parameters.transformBegin = 'translateX(0)'; 273 | parameters.transformEnd = 'translateX(' + innerWidth + 'px)'; 274 | parameters.distance = innerWidth; 275 | break; 276 | case kDirectionUp: 277 | parameters.transformBegin = 'translateY(' + innerHeight + 'px)'; 278 | parameters.transformEnd = 'translateY(0)'; 279 | parameters.distance = innerHeight; 280 | break; 281 | case kDirectionDown: 282 | parameters.transformBegin = 'translateY(0)'; 283 | parameters.transformEnd = 'translateY(' + innerHeight + 'px)'; 284 | parameters.distance = innerHeight; 285 | break; 286 | } 287 | 288 | if (this.loopCount_ % 2) { 289 | var transform = parameters.transformBegin; 290 | parameters.transformBegin = parameters.transformEnd; 291 | parameters.transformEnd = transform; 292 | } 293 | 294 | break; 295 | case kBehaviorSlide: 296 | switch (this.direction) { 297 | case kDirectionLeft: 298 | default: 299 | parameters.transformBegin = 'translateX(' + metrics.marqueeWidth + 'px)'; 300 | parameters.transformEnd = 'translateX(0)'; 301 | parameters.distance = metrics.marqueeWidth; 302 | break; 303 | case kDirectionRight: 304 | parameters.transformBegin = 'translateX(-' + metrics.contentWidth + 'px)'; 305 | parameters.transformEnd = 'translateX(' + innerWidth + 'px)'; 306 | parameters.distance = metrics.marqueeWidth; 307 | break; 308 | case kDirectionUp: 309 | parameters.transformBegin = 'translateY(' + metrics.marqueeHeight + 'px)'; 310 | parameters.transformEnd = 'translateY(0)'; 311 | parameters.distance = metrics.marqueeHeight; 312 | break; 313 | case kDirectionDown: 314 | parameters.transformBegin = 'translateY(-' + metrics.contentHeight + 'px)'; 315 | parameters.transformEnd = 'translateY(' + innerHeight + 'px)'; 316 | parameters.distance = metrics.marqueeHeight; 317 | break; 318 | } 319 | break; 320 | } 321 | 322 | return parameters 323 | }; 324 | 325 | HTMLMarqueeElementPrototype.shouldContinue_ = function() { 326 | var loop = this.loop; 327 | 328 | // By default, slide loops only once. 329 | if (loop <= 0 && this.behavior === kBehaviorSlide) 330 | loop = 1; 331 | 332 | if (loop <= 0) 333 | return true; 334 | return this.loopCount_ < loop; 335 | }; 336 | 337 | HTMLMarqueeElementPrototype.continue_ = function() { 338 | if (!this.shouldContinue_()) { 339 | this.player_ = null; 340 | this.dispatchEvent(new Event('finish', false, true)); 341 | return; 342 | } 343 | 344 | var parameters = this.getAnimationParameters_(); 345 | 346 | var player = this.mover_.animate([ 347 | { transform: parameters.transformBegin }, 348 | { transform: parameters.transformEnd }, 349 | ], { 350 | duration: parameters.distance * this.scrollDelay / this.scrollAmount, 351 | fill: 'forwards', 352 | }); 353 | 354 | this.player_ = player; 355 | 356 | player.addEventListener('finish', function() { 357 | if (player != this.player_) 358 | return; 359 | ++this.loopCount_; 360 | this.continue_(); 361 | if (this.player_ && this.behavior === kBehaviorAlternate) 362 | this.dispatchEvent(new Event('bounce', false, true)); 363 | }.bind(this)); 364 | }; 365 | 366 | HTMLMarqueeElementPrototype.start = function() { 367 | if (this.continueCallback_ || this.player_) 368 | return; 369 | this.continueCallback_ = global.requestAnimationFrame(function() { 370 | this.continueCallback_ = null; 371 | this.continue_(); 372 | }.bind(this)); 373 | this.dispatchEvent(new Event('start', false, true)); 374 | }; 375 | 376 | HTMLMarqueeElementPrototype.stop = function() { 377 | if (!this.continueCallback_ && !this.player_) 378 | return; 379 | 380 | if (this.continueCallback_) { 381 | global.cancelAnimationFrame(this.continueCallback_); 382 | this.continueCallback_ = null; 383 | return; 384 | } 385 | 386 | // TODO(abarth): Rather than canceling the animation, we really should just 387 | // pause the animation, but the pause function is still flagged as 388 | // experimental. 389 | if (this.player_) { 390 | var player = this.player_; 391 | this.player_ = null; 392 | player.cancel(); 393 | } 394 | }; 395 | 396 | global.document.registerElement('i-marquee', { 397 | prototype: HTMLMarqueeElementPrototype, 398 | }); 399 | 400 | })(this); 401 | --------------------------------------------------------------------------------