Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / core / html / HTMLMarqueeElement.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 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                 throw new DOMExceptionInPrivateScript("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                 throw new DOMExceptionInPrivateScript("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                 throw new DOMExceptionInPrivateScript("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 });