blob: eaf30f4dbb86313f8afcc02f6040897dd3eeaa3e [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001/**
2 * @license
3 * Copyright 2015 Google Inc. All Rights Reserved.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18(function() {
19 'use strict';
20
21 /**
22 * Class constructor for Ripple MDL component.
23 * Implements MDL component design pattern defined at:
24 * https://github.com/jasonmayes/mdl-component-design-pattern
25 *
26 * @constructor
27 * @param {HTMLElement} element The element that will be upgraded.
28 */
29 var MaterialRipple = function MaterialRipple(element) {
30 this.element_ = element;
31
32 // Initialize instance.
33 this.init();
34 };
35 window['MaterialRipple'] = MaterialRipple;
36
37 /**
38 * Store constants in one place so they can be updated easily.
39 *
40 * @enum {string | number}
41 * @private
42 */
43 MaterialRipple.prototype.Constant_ = {
44 INITIAL_SCALE: 'scale(0.0001, 0.0001)',
45 INITIAL_SIZE: '1px',
46 INITIAL_OPACITY: '0.4',
47 FINAL_OPACITY: '0',
48 FINAL_SCALE: ''
49 };
50
51 /**
52 * Store strings for class names defined by this component that are used in
53 * JavaScript. This allows us to simply change it in one place should we
54 * decide to modify at a later date.
55 *
56 * @enum {string}
57 * @private
58 */
59 MaterialRipple.prototype.CssClasses_ = {
60 RIPPLE_CENTER: 'mdl-ripple--center',
61 RIPPLE_EFFECT_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
62 RIPPLE: 'mdl-ripple',
63 IS_ANIMATING: 'is-animating',
64 IS_VISIBLE: 'is-visible'
65 };
66
67 /**
68 * Handle mouse / finger down on element.
69 *
70 * @param {Event} event The event that fired.
71 * @private
72 */
73 MaterialRipple.prototype.downHandler_ = function(event) {
74 if (!this.rippleElement_.style.width && !this.rippleElement_.style.height) {
75 var rect = this.element_.getBoundingClientRect();
76 this.boundHeight = rect.height;
77 this.boundWidth = rect.width;
78 this.rippleSize_ = Math.sqrt(rect.width * rect.width +
79 rect.height * rect.height) * 2 + 2;
80 this.rippleElement_.style.width = this.rippleSize_ + 'px';
81 this.rippleElement_.style.height = this.rippleSize_ + 'px';
82 }
83
84 this.rippleElement_.classList.add(this.CssClasses_.IS_VISIBLE);
85
86 if (event.type === 'mousedown' && this.ignoringMouseDown_) {
87 this.ignoringMouseDown_ = false;
88 } else {
89 if (event.type === 'touchstart') {
90 this.ignoringMouseDown_ = true;
91 }
92 var frameCount = this.getFrameCount();
93 if (frameCount > 0) {
94 return;
95 }
96 this.setFrameCount(1);
97 var bound = event.currentTarget.getBoundingClientRect();
98 var x;
99 var y;
100 // Check if we are handling a keyboard click.
101 if (event.clientX === 0 && event.clientY === 0) {
102 x = Math.round(bound.width / 2);
103 y = Math.round(bound.height / 2);
104 } else {
105 var clientX = event.clientX !== undefined ? event.clientX : event.touches[0].clientX;
106 var clientY = event.clientY !== undefined ? event.clientY : event.touches[0].clientY;
107 x = Math.round(clientX - bound.left);
108 y = Math.round(clientY - bound.top);
109 }
110 this.setRippleXY(x, y);
111 this.setRippleStyles(true);
112 window.requestAnimationFrame(this.animFrameHandler.bind(this));
113 }
114 };
115
116 /**
117 * Handle mouse / finger up on element.
118 *
119 * @param {Event} event The event that fired.
120 * @private
121 */
122 MaterialRipple.prototype.upHandler_ = function(event) {
123 // Don't fire for the artificial "mouseup" generated by a double-click.
124 if (event && event.detail !== 2) {
125 // Allow a repaint to occur before removing this class, so the animation
126 // shows for tap events, which seem to trigger a mouseup too soon after
127 // mousedown.
128 window.setTimeout(function() {
129 this.rippleElement_.classList.remove(this.CssClasses_.IS_VISIBLE);
130 }.bind(this), 0);
131 }
132 };
133
134 /**
135 * Initialize element.
136 */
137 MaterialRipple.prototype.init = function() {
138 if (this.element_) {
139 var recentering =
140 this.element_.classList.contains(this.CssClasses_.RIPPLE_CENTER);
141 if (!this.element_.classList.contains(
142 this.CssClasses_.RIPPLE_EFFECT_IGNORE_EVENTS)) {
143 this.rippleElement_ = this.element_.querySelector('.' +
144 this.CssClasses_.RIPPLE);
145 this.frameCount_ = 0;
146 this.rippleSize_ = 0;
147 this.x_ = 0;
148 this.y_ = 0;
149
150 // Touch start produces a compat mouse down event, which would cause a
151 // second ripples. To avoid that, we use this property to ignore the first
152 // mouse down after a touch start.
153 this.ignoringMouseDown_ = false;
154
155 this.boundDownHandler = this.downHandler_.bind(this);
156 this.element_.addEventListener('mousedown',
157 this.boundDownHandler);
158 this.element_.addEventListener('touchstart',
159 this.boundDownHandler);
160
161 this.boundUpHandler = this.upHandler_.bind(this);
162 this.element_.addEventListener('mouseup', this.boundUpHandler);
163 this.element_.addEventListener('mouseleave', this.boundUpHandler);
164 this.element_.addEventListener('touchend', this.boundUpHandler);
165 this.element_.addEventListener('blur', this.boundUpHandler);
166
167 /**
168 * Getter for frameCount_.
169 * @return {number} the frame count.
170 */
171 this.getFrameCount = function() {
172 return this.frameCount_;
173 };
174
175 /**
176 * Setter for frameCount_.
177 * @param {number} fC the frame count.
178 */
179 this.setFrameCount = function(fC) {
180 this.frameCount_ = fC;
181 };
182
183 /**
184 * Getter for rippleElement_.
185 * @return {Element} the ripple element.
186 */
187 this.getRippleElement = function() {
188 return this.rippleElement_;
189 };
190
191 /**
192 * Sets the ripple X and Y coordinates.
193 * @param {number} newX the new X coordinate
194 * @param {number} newY the new Y coordinate
195 */
196 this.setRippleXY = function(newX, newY) {
197 this.x_ = newX;
198 this.y_ = newY;
199 };
200
201 /**
202 * Sets the ripple styles.
203 * @param {boolean} start whether or not this is the start frame.
204 */
205 this.setRippleStyles = function(start) {
206 if (this.rippleElement_ !== null) {
207 var transformString;
208 var scale;
209 var size;
210 var offset = 'translate(' + this.x_ + 'px, ' + this.y_ + 'px)';
211
212 if (start) {
213 scale = this.Constant_.INITIAL_SCALE;
214 size = this.Constant_.INITIAL_SIZE;
215 } else {
216 scale = this.Constant_.FINAL_SCALE;
217 size = this.rippleSize_ + 'px';
218 if (recentering) {
219 offset = 'translate(' + this.boundWidth / 2 + 'px, ' +
220 this.boundHeight / 2 + 'px)';
221 }
222 }
223
224 transformString = 'translate(-50%, -50%) ' + offset + scale;
225
226 this.rippleElement_.style.webkitTransform = transformString;
227 this.rippleElement_.style.msTransform = transformString;
228 this.rippleElement_.style.transform = transformString;
229
230 if (start) {
231 this.rippleElement_.classList.remove(this.CssClasses_.IS_ANIMATING);
232 } else {
233 this.rippleElement_.classList.add(this.CssClasses_.IS_ANIMATING);
234 }
235 }
236 };
237
238 /**
239 * Handles an animation frame.
240 */
241 this.animFrameHandler = function() {
242 if (this.frameCount_-- > 0) {
243 window.requestAnimationFrame(this.animFrameHandler.bind(this));
244 } else {
245 this.setRippleStyles(false);
246 }
247 };
248 }
249 }
250 };
251
252 // The component registers itself. It can assume componentHandler is available
253 // in the global scope.
254 componentHandler.register({
255 constructor: MaterialRipple,
256 classAsString: 'MaterialRipple',
257 cssClass: 'mdl-js-ripple-effect',
258 widget: false
259 });
260})();