├── 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 |
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 |
--------------------------------------------------------------------------------