material_design_lite-sass 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +108 -0
- data/Rakefile +4 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/material_design_lite-sass.rb +46 -0
- data/lib/material_design_lite/sass/engine.rb +13 -0
- data/lib/material_design_lite/sass/version.rb +5 -0
- data/material_design_lite-sass.gemspec +28 -0
- data/vendor/assets/javascripts/material.js +3919 -0
- data/vendor/assets/javascripts/material/button.js +132 -0
- data/vendor/assets/javascripts/material/checkbox.js +265 -0
- data/vendor/assets/javascripts/material/data-table.js +149 -0
- data/vendor/assets/javascripts/material/icon-toggle.js +248 -0
- data/vendor/assets/javascripts/material/layout.js +434 -0
- data/vendor/assets/javascripts/material/mdlComponentHandler.js +346 -0
- data/vendor/assets/javascripts/material/menu.js +468 -0
- data/vendor/assets/javascripts/material/progress.js +116 -0
- data/vendor/assets/javascripts/material/rAF.js +38 -0
- data/vendor/assets/javascripts/material/radio.js +257 -0
- data/vendor/assets/javascripts/material/ripple.js +244 -0
- data/vendor/assets/javascripts/material/slider.js +252 -0
- data/vendor/assets/javascripts/material/spinner.js +140 -0
- data/vendor/assets/javascripts/material/switch.js +269 -0
- data/vendor/assets/javascripts/material/tabs.js +152 -0
- data/vendor/assets/javascripts/material/textfield.js +247 -0
- data/vendor/assets/javascripts/material/tooltip.js +146 -0
- data/vendor/assets/stylesheets/_material.scss +50 -0
- data/vendor/assets/stylesheets/material/_animation.scss +34 -0
- data/vendor/assets/stylesheets/material/_badge.scss +66 -0
- data/vendor/assets/stylesheets/material/_button.scss +298 -0
- data/vendor/assets/stylesheets/material/_card.scss +111 -0
- data/vendor/assets/stylesheets/material/_checkbox.scss +175 -0
- data/vendor/assets/stylesheets/material/_color-definitions.scss +599 -0
- data/vendor/assets/stylesheets/material/_data-table.scss +105 -0
- data/vendor/assets/stylesheets/material/_functions.scss +3 -0
- data/vendor/assets/stylesheets/material/_grid.scss +180 -0
- data/vendor/assets/stylesheets/material/_icon-toggle.scss +121 -0
- data/vendor/assets/stylesheets/material/_layout.scss +580 -0
- data/vendor/assets/stylesheets/material/_mega_footer.scss +309 -0
- data/vendor/assets/stylesheets/material/_menu.scss +193 -0
- data/vendor/assets/stylesheets/material/_mini_footer.scss +88 -0
- data/vendor/assets/stylesheets/material/_mixins.scss +268 -0
- data/vendor/assets/stylesheets/material/_palette.scss +2303 -0
- data/vendor/assets/stylesheets/material/_progress.scss +115 -0
- data/vendor/assets/stylesheets/material/_radio.scss +155 -0
- data/vendor/assets/stylesheets/material/_resets.scss +55 -0
- data/vendor/assets/stylesheets/material/_ripple.scss +42 -0
- data/vendor/assets/stylesheets/material/_shadow.scss +42 -0
- data/vendor/assets/stylesheets/material/_slider.scss +396 -0
- data/vendor/assets/stylesheets/material/_spinner.scss +248 -0
- data/vendor/assets/stylesheets/material/_switch.scss +199 -0
- data/vendor/assets/stylesheets/material/_tabs.scss +115 -0
- data/vendor/assets/stylesheets/material/_textfield.scss +190 -0
- data/vendor/assets/stylesheets/material/_tooltip.scss +66 -0
- data/vendor/assets/stylesheets/material/_typography.scss +297 -0
- data/vendor/assets/stylesheets/material/_variables.scss +572 -0
- data/vendor/assets/stylesheets/material/resets/_h5bp.scss +284 -0
- data/vendor/assets/stylesheets/material/resets/_mobile.scss +25 -0
- metadata +151 -0
@@ -0,0 +1,346 @@
|
|
1
|
+
/**
|
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
|
+
/**
|
19
|
+
* A component handler interface using the revealing module design pattern.
|
20
|
+
* More details on this design pattern here:
|
21
|
+
* https://github.com/jasonmayes/mdl-component-design-pattern
|
22
|
+
* @author Jason Mayes.
|
23
|
+
*/
|
24
|
+
/* exported componentHandler */
|
25
|
+
var componentHandler = (function() {
|
26
|
+
'use strict';
|
27
|
+
|
28
|
+
var registeredComponents_ = [];
|
29
|
+
var createdComponents_ = [];
|
30
|
+
var downgradeMethod_ = 'mdlDowngrade_';
|
31
|
+
var componentConfigProperty_ = 'mdlComponentConfigInternal_';
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Searches registered components for a class we are interested in using.
|
35
|
+
* Optionally replaces a match with passed object if specified.
|
36
|
+
* @param {string} name The name of a class we want to use.
|
37
|
+
* @param {Object=} optReplace Optional object to replace match with.
|
38
|
+
* @return {Object | boolean}
|
39
|
+
* @private
|
40
|
+
*/
|
41
|
+
function findRegisteredClass_(name, optReplace) {
|
42
|
+
for (var i = 0; i < registeredComponents_.length; i++) {
|
43
|
+
if (registeredComponents_[i].className === name) {
|
44
|
+
if (optReplace !== undefined) {
|
45
|
+
registeredComponents_[i] = optReplace;
|
46
|
+
}
|
47
|
+
return registeredComponents_[i];
|
48
|
+
}
|
49
|
+
}
|
50
|
+
return false;
|
51
|
+
}
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Returns an array of the classNames of the upgraded classes on the element.
|
55
|
+
* @param {HTMLElement} element The element to fetch data from.
|
56
|
+
* @return {Array<string>}
|
57
|
+
* @private
|
58
|
+
*/
|
59
|
+
function getUpgradedListOfElement_(element) {
|
60
|
+
var dataUpgraded = element.getAttribute('data-upgraded');
|
61
|
+
// Use `['']` as default value to conform the `,name,name...` style.
|
62
|
+
return dataUpgraded === null ? [''] : dataUpgraded.split(',');
|
63
|
+
}
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Returns true if the given element has already been upgraded for the given
|
67
|
+
* class.
|
68
|
+
* @param {HTMLElement} element The element we want to check.
|
69
|
+
* @param {string} jsClass The class to check for.
|
70
|
+
* @return boolean
|
71
|
+
* @private
|
72
|
+
*/
|
73
|
+
function isElementUpgraded_(element, jsClass) {
|
74
|
+
var upgradedList = getUpgradedListOfElement_(element);
|
75
|
+
return upgradedList.indexOf(jsClass) !== -1;
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Searches existing DOM for elements of our component type and upgrades them
|
80
|
+
* if they have not already been upgraded.
|
81
|
+
* @param {!string=} optJsClass the programatic name of the element class we
|
82
|
+
* need to create a new instance of.
|
83
|
+
* @param {!string=} optCssClass the name of the CSS class elements of this
|
84
|
+
* type will have.
|
85
|
+
*/
|
86
|
+
function upgradeDomInternal(optJsClass, optCssClass) {
|
87
|
+
if (optJsClass === undefined && optCssClass === undefined) {
|
88
|
+
for (var i = 0; i < registeredComponents_.length; i++) {
|
89
|
+
upgradeDomInternal(registeredComponents_[i].className,
|
90
|
+
registeredComponents_[i].cssClass);
|
91
|
+
}
|
92
|
+
} else {
|
93
|
+
var jsClass = /** @type {!string} */ (optJsClass);
|
94
|
+
if (optCssClass === undefined) {
|
95
|
+
var registeredClass = findRegisteredClass_(jsClass);
|
96
|
+
if (registeredClass) {
|
97
|
+
optCssClass = registeredClass.cssClass;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
var elements = document.querySelectorAll('.' + optCssClass);
|
102
|
+
for (var n = 0; n < elements.length; n++) {
|
103
|
+
upgradeElementInternal(elements[n], jsClass);
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Upgrades a specific element rather than all in the DOM.
|
110
|
+
* @param {HTMLElement} element The element we wish to upgrade.
|
111
|
+
* @param {!string=} optJsClass Optional name of the class we want to upgrade
|
112
|
+
* the element to.
|
113
|
+
*/
|
114
|
+
function upgradeElementInternal(element, optJsClass) {
|
115
|
+
// Verify argument type.
|
116
|
+
if (!(typeof element === 'object' && element instanceof Element)) {
|
117
|
+
throw new Error('Invalid argument provided to upgrade MDL element.');
|
118
|
+
}
|
119
|
+
var upgradedList = getUpgradedListOfElement_(element);
|
120
|
+
var classesToUpgrade = [];
|
121
|
+
// If jsClass is not provided scan the registered components to find the
|
122
|
+
// ones matching the element's CSS classList.
|
123
|
+
if (!optJsClass) {
|
124
|
+
var classList = element.classList;
|
125
|
+
registeredComponents_.forEach(function (component) {
|
126
|
+
// Match CSS & Not to be upgraded & Not upgraded.
|
127
|
+
if (classList.contains(component.cssClass) &&
|
128
|
+
classesToUpgrade.indexOf(component) === -1 &&
|
129
|
+
!isElementUpgraded_(element, component.className)) {
|
130
|
+
classesToUpgrade.push(component);
|
131
|
+
}
|
132
|
+
});
|
133
|
+
} else if (!isElementUpgraded_(element, optJsClass)) {
|
134
|
+
classesToUpgrade.push(findRegisteredClass_(optJsClass));
|
135
|
+
}
|
136
|
+
|
137
|
+
// Upgrade the element for each classes.
|
138
|
+
for (var i = 0, n = classesToUpgrade.length, registeredClass; i < n; i++) {
|
139
|
+
registeredClass = classesToUpgrade[i];
|
140
|
+
if (registeredClass) {
|
141
|
+
// Mark element as upgraded.
|
142
|
+
upgradedList.push(registeredClass.className);
|
143
|
+
element.setAttribute('data-upgraded', upgradedList.join(','));
|
144
|
+
var instance = new registeredClass.classConstructor(element);
|
145
|
+
instance[componentConfigProperty_] = registeredClass;
|
146
|
+
createdComponents_.push(instance);
|
147
|
+
// Call any callbacks the user has registered with this component type.
|
148
|
+
for (var j = 0, m = registeredClass.callbacks.length; j < m; j++) {
|
149
|
+
registeredClass.callbacks[j](element);
|
150
|
+
}
|
151
|
+
|
152
|
+
if (registeredClass.widget) {
|
153
|
+
// Assign per element instance for control over API
|
154
|
+
element[registeredClass.className] = instance;
|
155
|
+
}
|
156
|
+
} else {
|
157
|
+
throw new Error(
|
158
|
+
'Unable to find a registered component for the given class.');
|
159
|
+
}
|
160
|
+
|
161
|
+
var ev = document.createEvent('Events');
|
162
|
+
ev.initEvent('mdl-componentupgraded', true, true);
|
163
|
+
element.dispatchEvent(ev);
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
/**
|
168
|
+
* Upgrades a specific list of elements rather than all in the DOM.
|
169
|
+
* @param {HTMLElement | Array<HTMLElement> | NodeList | HTMLCollection} elements
|
170
|
+
* The elements we wish to upgrade.
|
171
|
+
*/
|
172
|
+
function upgradeElementsInternal(elements) {
|
173
|
+
if (!Array.isArray(elements)) {
|
174
|
+
if (typeof elements.item === 'function') {
|
175
|
+
elements = Array.prototype.slice.call(elements);
|
176
|
+
} else {
|
177
|
+
elements = [elements];
|
178
|
+
}
|
179
|
+
}
|
180
|
+
for (var i = 0, n = elements.length, element; i < n; i++) {
|
181
|
+
element = elements[i];
|
182
|
+
if (element instanceof HTMLElement) {
|
183
|
+
if (element.children.length > 0) {
|
184
|
+
upgradeElementsInternal(element.children);
|
185
|
+
}
|
186
|
+
upgradeElementInternal(element);
|
187
|
+
}
|
188
|
+
}
|
189
|
+
}
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Registers a class for future use and attempts to upgrade existing DOM.
|
193
|
+
* @param {Object} config An object containing:
|
194
|
+
* {constructor: Constructor, classAsString: string, cssClass: string}
|
195
|
+
*/
|
196
|
+
function registerInternal(config) {
|
197
|
+
var newConfig = {
|
198
|
+
'classConstructor': config.constructor,
|
199
|
+
'className': config.classAsString,
|
200
|
+
'cssClass': config.cssClass,
|
201
|
+
'widget': config.widget === undefined ? true : config.widget,
|
202
|
+
'callbacks': []
|
203
|
+
};
|
204
|
+
|
205
|
+
registeredComponents_.forEach(function(item) {
|
206
|
+
if (item.cssClass === newConfig.cssClass) {
|
207
|
+
throw new Error('The provided cssClass has already been registered.');
|
208
|
+
}
|
209
|
+
if (item.className === newConfig.className) {
|
210
|
+
throw new Error('The provided className has already been registered');
|
211
|
+
}
|
212
|
+
});
|
213
|
+
|
214
|
+
if (config.constructor.prototype
|
215
|
+
.hasOwnProperty(componentConfigProperty_)) {
|
216
|
+
throw new Error(
|
217
|
+
'MDL component classes must not have ' + componentConfigProperty_ +
|
218
|
+
' defined as a property.');
|
219
|
+
}
|
220
|
+
|
221
|
+
var found = findRegisteredClass_(config.classAsString, newConfig);
|
222
|
+
|
223
|
+
if (!found) {
|
224
|
+
registeredComponents_.push(newConfig);
|
225
|
+
}
|
226
|
+
}
|
227
|
+
|
228
|
+
/**
|
229
|
+
* Allows user to be alerted to any upgrades that are performed for a given
|
230
|
+
* component type
|
231
|
+
* @param {string} jsClass The class name of the MDL component we wish
|
232
|
+
* to hook into for any upgrades performed.
|
233
|
+
* @param {!Function} callback The function to call upon an upgrade. This
|
234
|
+
* function should expect 1 parameter - the HTMLElement which got upgraded.
|
235
|
+
*/
|
236
|
+
function registerUpgradedCallbackInternal(jsClass, callback) {
|
237
|
+
var regClass = findRegisteredClass_(jsClass);
|
238
|
+
if (regClass) {
|
239
|
+
regClass.callbacks.push(callback);
|
240
|
+
}
|
241
|
+
}
|
242
|
+
|
243
|
+
/**
|
244
|
+
* Upgrades all registered components found in the current DOM. This is
|
245
|
+
* automatically called on window load.
|
246
|
+
*/
|
247
|
+
function upgradeAllRegisteredInternal() {
|
248
|
+
for (var n = 0; n < registeredComponents_.length; n++) {
|
249
|
+
upgradeDomInternal(registeredComponents_[n].className);
|
250
|
+
}
|
251
|
+
}
|
252
|
+
|
253
|
+
/**
|
254
|
+
* Finds a created component by a given DOM node.
|
255
|
+
*
|
256
|
+
* @param {!Element} node
|
257
|
+
* @return {*}
|
258
|
+
*/
|
259
|
+
function findCreatedComponentByNodeInternal(node) {
|
260
|
+
for (var n = 0; n < createdComponents_.length; n++) {
|
261
|
+
var component = createdComponents_[n];
|
262
|
+
if (component.element_ === node) {
|
263
|
+
return component;
|
264
|
+
}
|
265
|
+
}
|
266
|
+
}
|
267
|
+
|
268
|
+
/**
|
269
|
+
* Check the component for the downgrade method.
|
270
|
+
* Execute if found.
|
271
|
+
* Remove component from createdComponents list.
|
272
|
+
*
|
273
|
+
* @param {*} component
|
274
|
+
*/
|
275
|
+
function deconstructComponentInternal(component) {
|
276
|
+
if (component &&
|
277
|
+
component[componentConfigProperty_]
|
278
|
+
.classConstructor.prototype
|
279
|
+
.hasOwnProperty(downgradeMethod_)) {
|
280
|
+
component[downgradeMethod_]();
|
281
|
+
var componentIndex = createdComponents_.indexOf(component);
|
282
|
+
createdComponents_.splice(componentIndex, 1);
|
283
|
+
|
284
|
+
var upgrades = component.element_.dataset.upgraded.split(',');
|
285
|
+
var componentPlace = upgrades.indexOf(
|
286
|
+
component[componentConfigProperty_].classAsString);
|
287
|
+
upgrades.splice(componentPlace, 1);
|
288
|
+
component.element_.dataset.upgraded = upgrades.join(',');
|
289
|
+
|
290
|
+
var ev = document.createEvent('Events');
|
291
|
+
ev.initEvent('mdl-componentdowngraded', true, true);
|
292
|
+
component.element_.dispatchEvent(ev);
|
293
|
+
}
|
294
|
+
}
|
295
|
+
|
296
|
+
/**
|
297
|
+
* Downgrade either a given node, an array of nodes, or a NodeList.
|
298
|
+
*
|
299
|
+
* @param {*} nodes
|
300
|
+
*/
|
301
|
+
function downgradeNodesInternal(nodes) {
|
302
|
+
var downgradeNode = function(node) {
|
303
|
+
deconstructComponentInternal(findCreatedComponentByNodeInternal(node));
|
304
|
+
};
|
305
|
+
if (nodes instanceof Array || nodes instanceof NodeList) {
|
306
|
+
for (var n = 0; n < nodes.length; n++) {
|
307
|
+
downgradeNode(nodes[n]);
|
308
|
+
}
|
309
|
+
} else if (nodes instanceof Node) {
|
310
|
+
downgradeNode(nodes);
|
311
|
+
} else {
|
312
|
+
throw new Error('Invalid argument provided to downgrade MDL nodes.');
|
313
|
+
}
|
314
|
+
}
|
315
|
+
|
316
|
+
// Now return the functions that should be made public with their publicly
|
317
|
+
// facing names...
|
318
|
+
return {
|
319
|
+
upgradeDom: upgradeDomInternal,
|
320
|
+
upgradeElement: upgradeElementInternal,
|
321
|
+
upgradeElements: upgradeElementsInternal,
|
322
|
+
upgradeAllRegistered: upgradeAllRegisteredInternal,
|
323
|
+
registerUpgradedCallback: registerUpgradedCallbackInternal,
|
324
|
+
register: registerInternal,
|
325
|
+
downgradeElements: downgradeNodesInternal
|
326
|
+
};
|
327
|
+
})();
|
328
|
+
|
329
|
+
window.addEventListener('load', function() {
|
330
|
+
'use strict';
|
331
|
+
|
332
|
+
/**
|
333
|
+
* Performs a "Cutting the mustard" test. If the browser supports the features
|
334
|
+
* tested, adds a mdl-js class to the <html> element. It then upgrades all MDL
|
335
|
+
* components requiring JavaScript.
|
336
|
+
*/
|
337
|
+
if ('classList' in document.createElement('div') &&
|
338
|
+
'querySelector' in document &&
|
339
|
+
'addEventListener' in window && Array.prototype.forEach) {
|
340
|
+
document.documentElement.classList.add('mdl-js');
|
341
|
+
componentHandler.upgradeAllRegistered();
|
342
|
+
} else {
|
343
|
+
componentHandler.upgradeElement =
|
344
|
+
componentHandler.register = function() {};
|
345
|
+
}
|
346
|
+
});
|
@@ -0,0 +1,468 @@
|
|
1
|
+
/**
|
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
|
+
/**
|
19
|
+
* Class constructor for dropdown MDL component.
|
20
|
+
* Implements MDL component design pattern defined at:
|
21
|
+
* https://github.com/jasonmayes/mdl-component-design-pattern
|
22
|
+
* @param {HTMLElement} element The element that will be upgraded.
|
23
|
+
*/
|
24
|
+
function MaterialMenu(element) {
|
25
|
+
'use strict';
|
26
|
+
|
27
|
+
this.element_ = element;
|
28
|
+
|
29
|
+
// Initialize instance.
|
30
|
+
this.init();
|
31
|
+
}
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Store constants in one place so they can be updated easily.
|
35
|
+
* @enum {string | number}
|
36
|
+
* @private
|
37
|
+
*/
|
38
|
+
MaterialMenu.prototype.Constant_ = {
|
39
|
+
// Total duration of the menu animation.
|
40
|
+
TRANSITION_DURATION_SECONDS: 0.3,
|
41
|
+
// The fraction of the total duration we want to use for menu item animations.
|
42
|
+
TRANSITION_DURATION_FRACTION: 0.8,
|
43
|
+
// How long the menu stays open after choosing an option (so the user can see
|
44
|
+
// the ripple).
|
45
|
+
CLOSE_TIMEOUT: 150
|
46
|
+
};
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Keycodes, for code readability.
|
50
|
+
* @enum {number}
|
51
|
+
* @private
|
52
|
+
*/
|
53
|
+
MaterialMenu.prototype.Keycodes_ = {
|
54
|
+
ENTER: 13,
|
55
|
+
ESCAPE: 27,
|
56
|
+
SPACE: 32,
|
57
|
+
UP_ARROW: 38,
|
58
|
+
DOWN_ARROW: 40
|
59
|
+
};
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Store strings for class names defined by this component that are used in
|
63
|
+
* JavaScript. This allows us to simply change it in one place should we
|
64
|
+
* decide to modify at a later date.
|
65
|
+
* @enum {string}
|
66
|
+
* @private
|
67
|
+
*/
|
68
|
+
MaterialMenu.prototype.CssClasses_ = {
|
69
|
+
CONTAINER: 'mdl-menu__container',
|
70
|
+
OUTLINE: 'mdl-menu__outline',
|
71
|
+
ITEM: 'mdl-menu__item',
|
72
|
+
ITEM_RIPPLE_CONTAINER: 'mdl-menu__item-ripple-container',
|
73
|
+
RIPPLE_EFFECT: 'mdl-js-ripple-effect',
|
74
|
+
RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
|
75
|
+
RIPPLE: 'mdl-ripple',
|
76
|
+
// Statuses
|
77
|
+
IS_UPGRADED: 'is-upgraded',
|
78
|
+
IS_VISIBLE: 'is-visible',
|
79
|
+
IS_ANIMATING: 'is-animating',
|
80
|
+
// Alignment options
|
81
|
+
BOTTOM_LEFT: 'mdl-menu--bottom-left', // This is the default.
|
82
|
+
BOTTOM_RIGHT: 'mdl-menu--bottom-right',
|
83
|
+
TOP_LEFT: 'mdl-menu--top-left',
|
84
|
+
TOP_RIGHT: 'mdl-menu--top-right',
|
85
|
+
UNALIGNED: 'mdl-menu--unaligned'
|
86
|
+
};
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Initialize element.
|
90
|
+
*/
|
91
|
+
MaterialMenu.prototype.init = function() {
|
92
|
+
'use strict';
|
93
|
+
|
94
|
+
if (this.element_) {
|
95
|
+
// Create container for the menu.
|
96
|
+
var container = document.createElement('div');
|
97
|
+
container.classList.add(this.CssClasses_.CONTAINER);
|
98
|
+
this.element_.parentElement.insertBefore(container, this.element_);
|
99
|
+
this.element_.parentElement.removeChild(this.element_);
|
100
|
+
container.appendChild(this.element_);
|
101
|
+
this.container_ = container;
|
102
|
+
|
103
|
+
// Create outline for the menu (shadow and background).
|
104
|
+
var outline = document.createElement('div');
|
105
|
+
outline.classList.add(this.CssClasses_.OUTLINE);
|
106
|
+
this.outline_ = outline;
|
107
|
+
container.insertBefore(outline, this.element_);
|
108
|
+
|
109
|
+
// Find the "for" element and bind events to it.
|
110
|
+
var forElId = this.element_.getAttribute('for');
|
111
|
+
var forEl = null;
|
112
|
+
if (forElId) {
|
113
|
+
forEl = document.getElementById(forElId);
|
114
|
+
if (forEl) {
|
115
|
+
this.forElement_ = forEl;
|
116
|
+
forEl.addEventListener('click', this.handleForClick_.bind(this));
|
117
|
+
forEl.addEventListener('keydown',
|
118
|
+
this.handleForKeyboardEvent_.bind(this));
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
|
123
|
+
|
124
|
+
for (var i = 0; i < items.length; i++) {
|
125
|
+
// Add a listener to each menu item.
|
126
|
+
items[i].addEventListener('click', this.handleItemClick_.bind(this));
|
127
|
+
// Add a tab index to each menu item.
|
128
|
+
items[i].tabIndex = '-1';
|
129
|
+
// Add a keyboard listener to each menu item.
|
130
|
+
items[i].addEventListener('keydown',
|
131
|
+
this.handleItemKeyboardEvent_.bind(this));
|
132
|
+
}
|
133
|
+
|
134
|
+
// Add ripple classes to each item, if the user has enabled ripples.
|
135
|
+
if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {
|
136
|
+
this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
|
137
|
+
|
138
|
+
for (i = 0; i < items.length; i++) {
|
139
|
+
var item = items[i];
|
140
|
+
|
141
|
+
var rippleContainer = document.createElement('span');
|
142
|
+
rippleContainer.classList.add(this.CssClasses_.ITEM_RIPPLE_CONTAINER);
|
143
|
+
|
144
|
+
var ripple = document.createElement('span');
|
145
|
+
ripple.classList.add(this.CssClasses_.RIPPLE);
|
146
|
+
rippleContainer.appendChild(ripple);
|
147
|
+
|
148
|
+
item.appendChild(rippleContainer);
|
149
|
+
item.classList.add(this.CssClasses_.RIPPLE_EFFECT);
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
// Copy alignment classes to the container, so the outline can use them.
|
154
|
+
if (this.element_.classList.contains(this.CssClasses_.BOTTOM_LEFT)) {
|
155
|
+
this.outline_.classList.add(this.CssClasses_.BOTTOM_LEFT);
|
156
|
+
}
|
157
|
+
if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
|
158
|
+
this.outline_.classList.add(this.CssClasses_.BOTTOM_RIGHT);
|
159
|
+
}
|
160
|
+
if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
|
161
|
+
this.outline_.classList.add(this.CssClasses_.TOP_LEFT);
|
162
|
+
}
|
163
|
+
if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
|
164
|
+
this.outline_.classList.add(this.CssClasses_.TOP_RIGHT);
|
165
|
+
}
|
166
|
+
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
|
167
|
+
this.outline_.classList.add(this.CssClasses_.UNALIGNED);
|
168
|
+
}
|
169
|
+
|
170
|
+
container.classList.add(this.CssClasses_.IS_UPGRADED);
|
171
|
+
}
|
172
|
+
};
|
173
|
+
|
174
|
+
/**
|
175
|
+
* Handles a click on the "for" element, by positioning the menu and then
|
176
|
+
* toggling it.
|
177
|
+
* @private
|
178
|
+
*/
|
179
|
+
MaterialMenu.prototype.handleForClick_ = function(evt) {
|
180
|
+
'use strict';
|
181
|
+
|
182
|
+
if (this.element_ && this.forElement_) {
|
183
|
+
var rect = this.forElement_.getBoundingClientRect();
|
184
|
+
var forRect = this.forElement_.parentElement.getBoundingClientRect();
|
185
|
+
|
186
|
+
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
|
187
|
+
// Do not position the menu automatically. Requires the developer to
|
188
|
+
// manually specify position.
|
189
|
+
} else if (this.element_.classList.contains(
|
190
|
+
this.CssClasses_.BOTTOM_RIGHT)) {
|
191
|
+
// Position below the "for" element, aligned to its right.
|
192
|
+
this.container_.style.right = (forRect.right - rect.right) + 'px';
|
193
|
+
this.container_.style.top =
|
194
|
+
this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px';
|
195
|
+
} else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
|
196
|
+
// Position above the "for" element, aligned to its left.
|
197
|
+
this.container_.style.left = this.forElement_.offsetLeft + 'px';
|
198
|
+
this.container_.style.bottom = (forRect.bottom - rect.top) + 'px';
|
199
|
+
} else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
|
200
|
+
// Position above the "for" element, aligned to its right.
|
201
|
+
this.container_.style.right = (forRect.right - rect.right) + 'px';
|
202
|
+
this.container_.style.bottom = (forRect.bottom - rect.top) + 'px';
|
203
|
+
} else {
|
204
|
+
// Default: position below the "for" element, aligned to its left.
|
205
|
+
this.container_.style.left = this.forElement_.offsetLeft + 'px';
|
206
|
+
this.container_.style.top =
|
207
|
+
this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px';
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
this.toggle(evt);
|
212
|
+
};
|
213
|
+
|
214
|
+
/**
|
215
|
+
* Handles a keyboard event on the "for" element.
|
216
|
+
* @private
|
217
|
+
*/
|
218
|
+
MaterialMenu.prototype.handleForKeyboardEvent_ = function(evt) {
|
219
|
+
'use strict';
|
220
|
+
|
221
|
+
if (this.element_ && this.container_ && this.forElement_) {
|
222
|
+
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM +
|
223
|
+
':not([disabled])');
|
224
|
+
|
225
|
+
if (items && items.length > 0 &&
|
226
|
+
this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
|
227
|
+
if (evt.keyCode === this.Keycodes_.UP_ARROW) {
|
228
|
+
evt.preventDefault();
|
229
|
+
items[items.length - 1].focus();
|
230
|
+
} else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
|
231
|
+
evt.preventDefault();
|
232
|
+
items[0].focus();
|
233
|
+
}
|
234
|
+
}
|
235
|
+
}
|
236
|
+
};
|
237
|
+
|
238
|
+
/**
|
239
|
+
* Handles a keyboard event on an item.
|
240
|
+
* @private
|
241
|
+
*/
|
242
|
+
MaterialMenu.prototype.handleItemKeyboardEvent_ = function(evt) {
|
243
|
+
'use strict';
|
244
|
+
|
245
|
+
if (this.element_ && this.container_) {
|
246
|
+
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM +
|
247
|
+
':not([disabled])');
|
248
|
+
|
249
|
+
if (items && items.length > 0 &&
|
250
|
+
this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
|
251
|
+
var currentIndex = Array.prototype.slice.call(items).indexOf(evt.target);
|
252
|
+
|
253
|
+
if (evt.keyCode === this.Keycodes_.UP_ARROW) {
|
254
|
+
evt.preventDefault();
|
255
|
+
if (currentIndex > 0) {
|
256
|
+
items[currentIndex - 1].focus();
|
257
|
+
} else {
|
258
|
+
items[items.length - 1].focus();
|
259
|
+
}
|
260
|
+
} else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
|
261
|
+
evt.preventDefault();
|
262
|
+
if (items.length > currentIndex + 1) {
|
263
|
+
items[currentIndex + 1].focus();
|
264
|
+
} else {
|
265
|
+
items[0].focus();
|
266
|
+
}
|
267
|
+
} else if (evt.keyCode === this.Keycodes_.SPACE ||
|
268
|
+
evt.keyCode === this.Keycodes_.ENTER) {
|
269
|
+
evt.preventDefault();
|
270
|
+
// Send mousedown and mouseup to trigger ripple.
|
271
|
+
var e = new MouseEvent('mousedown');
|
272
|
+
evt.target.dispatchEvent(e);
|
273
|
+
e = new MouseEvent('mouseup');
|
274
|
+
evt.target.dispatchEvent(e);
|
275
|
+
// Send click.
|
276
|
+
evt.target.click();
|
277
|
+
} else if (evt.keyCode === this.Keycodes_.ESCAPE) {
|
278
|
+
evt.preventDefault();
|
279
|
+
this.hide();
|
280
|
+
}
|
281
|
+
}
|
282
|
+
}
|
283
|
+
};
|
284
|
+
|
285
|
+
/**
|
286
|
+
* Handles a click event on an item.
|
287
|
+
* @private
|
288
|
+
*/
|
289
|
+
MaterialMenu.prototype.handleItemClick_ = function(evt) {
|
290
|
+
'use strict';
|
291
|
+
|
292
|
+
if (evt.target.getAttribute('disabled') !== null) {
|
293
|
+
evt.stopPropagation();
|
294
|
+
} else {
|
295
|
+
// Wait some time before closing menu, so the user can see the ripple.
|
296
|
+
this.closing_ = true;
|
297
|
+
window.setTimeout(function(evt) {
|
298
|
+
this.hide();
|
299
|
+
this.closing_ = false;
|
300
|
+
}.bind(this), this.Constant_.CLOSE_TIMEOUT);
|
301
|
+
}
|
302
|
+
};
|
303
|
+
|
304
|
+
/**
|
305
|
+
* Calculates the initial clip (for opening the menu) or final clip (for closing
|
306
|
+
* it), and applies it. This allows us to animate from or to the correct point,
|
307
|
+
* that is, the point it's aligned to in the "for" element.
|
308
|
+
* @private
|
309
|
+
*/
|
310
|
+
MaterialMenu.prototype.applyClip_ = function(height, width) {
|
311
|
+
'use strict';
|
312
|
+
|
313
|
+
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
|
314
|
+
// Do not clip.
|
315
|
+
this.element_.style.clip = null;
|
316
|
+
} else if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
|
317
|
+
// Clip to the top right corner of the menu.
|
318
|
+
this.element_.style.clip =
|
319
|
+
'rect(0 ' + width + 'px ' + '0 ' + width + 'px)';
|
320
|
+
} else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
|
321
|
+
// Clip to the bottom left corner of the menu.
|
322
|
+
this.element_.style.clip =
|
323
|
+
'rect(' + height + 'px 0 ' + height + 'px 0)';
|
324
|
+
} else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
|
325
|
+
// Clip to the bottom right corner of the menu.
|
326
|
+
this.element_.style.clip = 'rect(' + height + 'px ' + width + 'px ' +
|
327
|
+
height + 'px ' + width + 'px)';
|
328
|
+
} else {
|
329
|
+
// Default: do not clip (same as clipping to the top left corner).
|
330
|
+
this.element_.style.clip = null;
|
331
|
+
}
|
332
|
+
};
|
333
|
+
|
334
|
+
/**
|
335
|
+
* Adds an event listener to clean up after the animation ends.
|
336
|
+
* @private
|
337
|
+
*/
|
338
|
+
MaterialMenu.prototype.addAnimationEndListener_ = function() {
|
339
|
+
'use strict';
|
340
|
+
|
341
|
+
var cleanup = function () {
|
342
|
+
this.element_.removeEventListener('transitionend', cleanup);
|
343
|
+
this.element_.removeEventListener('webkitTransitionEnd', cleanup);
|
344
|
+
this.element_.classList.remove(this.CssClasses_.IS_ANIMATING);
|
345
|
+
}.bind(this);
|
346
|
+
|
347
|
+
// Remove animation class once the transition is done.
|
348
|
+
this.element_.addEventListener('transitionend', cleanup);
|
349
|
+
this.element_.addEventListener('webkitTransitionEnd', cleanup);
|
350
|
+
};
|
351
|
+
|
352
|
+
/**
|
353
|
+
* Displays the menu.
|
354
|
+
* @public
|
355
|
+
*/
|
356
|
+
MaterialMenu.prototype.show = function(evt) {
|
357
|
+
'use strict';
|
358
|
+
|
359
|
+
if (this.element_ && this.container_ && this.outline_) {
|
360
|
+
// Measure the inner element.
|
361
|
+
var height = this.element_.getBoundingClientRect().height;
|
362
|
+
var width = this.element_.getBoundingClientRect().width;
|
363
|
+
|
364
|
+
// Apply the inner element's size to the container and outline.
|
365
|
+
this.container_.style.width = width + 'px';
|
366
|
+
this.container_.style.height = height + 'px';
|
367
|
+
this.outline_.style.width = width + 'px';
|
368
|
+
this.outline_.style.height = height + 'px';
|
369
|
+
|
370
|
+
var transitionDuration = this.Constant_.TRANSITION_DURATION_SECONDS *
|
371
|
+
this.Constant_.TRANSITION_DURATION_FRACTION;
|
372
|
+
|
373
|
+
// Calculate transition delays for individual menu items, so that they fade
|
374
|
+
// in one at a time.
|
375
|
+
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
|
376
|
+
for (var i = 0; i < items.length; i++) {
|
377
|
+
var itemDelay = null;
|
378
|
+
if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT) ||
|
379
|
+
this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
|
380
|
+
itemDelay = ((height - items[i].offsetTop - items[i].offsetHeight) /
|
381
|
+
height * transitionDuration) + 's';
|
382
|
+
} else {
|
383
|
+
itemDelay = (items[i].offsetTop / height * transitionDuration) + 's';
|
384
|
+
}
|
385
|
+
items[i].style.transitionDelay = itemDelay;
|
386
|
+
}
|
387
|
+
|
388
|
+
// Apply the initial clip to the text before we start animating.
|
389
|
+
this.applyClip_(height, width);
|
390
|
+
|
391
|
+
// Wait for the next frame, turn on animation, and apply the final clip.
|
392
|
+
// Also make it visible. This triggers the transitions.
|
393
|
+
window.requestAnimationFrame(function() {
|
394
|
+
this.element_.classList.add(this.CssClasses_.IS_ANIMATING);
|
395
|
+
this.element_.style.clip = 'rect(0 ' + width + 'px ' + height + 'px 0)';
|
396
|
+
this.container_.classList.add(this.CssClasses_.IS_VISIBLE);
|
397
|
+
}.bind(this));
|
398
|
+
|
399
|
+
// Clean up after the animation is complete.
|
400
|
+
this.addAnimationEndListener_();
|
401
|
+
|
402
|
+
// Add a click listener to the document, to close the menu.
|
403
|
+
var callback = function(e) {
|
404
|
+
// Check to see if the document is processing the same event that
|
405
|
+
// displayed the menu in the first place. If so, do nothing.
|
406
|
+
// Also check to see if the menu is in the process of closing itself, and
|
407
|
+
// do nothing in that case.
|
408
|
+
if (e !== evt && !this.closing_) {
|
409
|
+
document.removeEventListener('click', callback);
|
410
|
+
this.hide();
|
411
|
+
}
|
412
|
+
}.bind(this);
|
413
|
+
document.addEventListener('click', callback);
|
414
|
+
}
|
415
|
+
};
|
416
|
+
|
417
|
+
/**
|
418
|
+
* Hides the menu.
|
419
|
+
* @public
|
420
|
+
*/
|
421
|
+
MaterialMenu.prototype.hide = function() {
|
422
|
+
'use strict';
|
423
|
+
|
424
|
+
if (this.element_ && this.container_ && this.outline_) {
|
425
|
+
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
|
426
|
+
|
427
|
+
// Remove all transition delays; menu items fade out concurrently.
|
428
|
+
for (var i = 0; i < items.length; i++) {
|
429
|
+
items[i].style.transitionDelay = null;
|
430
|
+
}
|
431
|
+
|
432
|
+
// Measure the inner element.
|
433
|
+
var height = this.element_.getBoundingClientRect().height;
|
434
|
+
var width = this.element_.getBoundingClientRect().width;
|
435
|
+
|
436
|
+
// Turn on animation, and apply the final clip. Also make invisible.
|
437
|
+
// This triggers the transitions.
|
438
|
+
this.element_.classList.add(this.CssClasses_.IS_ANIMATING);
|
439
|
+
this.applyClip_(height, width);
|
440
|
+
this.container_.classList.remove(this.CssClasses_.IS_VISIBLE);
|
441
|
+
|
442
|
+
// Clean up after the animation is complete.
|
443
|
+
this.addAnimationEndListener_();
|
444
|
+
}
|
445
|
+
};
|
446
|
+
|
447
|
+
/**
|
448
|
+
* Displays or hides the menu, depending on current state.
|
449
|
+
* @public
|
450
|
+
*/
|
451
|
+
MaterialMenu.prototype.toggle = function(evt) {
|
452
|
+
'use strict';
|
453
|
+
|
454
|
+
if (this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
|
455
|
+
this.hide();
|
456
|
+
} else {
|
457
|
+
this.show(evt);
|
458
|
+
}
|
459
|
+
};
|
460
|
+
|
461
|
+
// The component registers itself. It can assume componentHandler is available
|
462
|
+
// in the global scope.
|
463
|
+
componentHandler.register({
|
464
|
+
constructor: MaterialMenu,
|
465
|
+
classAsString: 'MaterialMenu',
|
466
|
+
cssClass: 'mdl-js-menu',
|
467
|
+
widget: true
|
468
|
+
});
|