kmaterialize 2.0.0

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.
package/js/forms.js ADDED
@@ -0,0 +1,275 @@
1
+ (function($) {
2
+ // Function to update labels of text fields
3
+ M.updateTextFields = function() {
4
+ let input_selector =
5
+ 'input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], input[type=number], input[type=search], input[type=date], input[type=time], textarea';
6
+ $(input_selector).each(function(element, index) {
7
+ let $this = $(this);
8
+ if (
9
+ element.value.length > 0 ||
10
+ $(element).is(':focus') ||
11
+ element.autofocus ||
12
+ $this.attr('placeholder') !== null
13
+ ) {
14
+ $this.siblings('label').addClass('active');
15
+ } else if (element.validity) {
16
+ $this.siblings('label').toggleClass('active', element.validity.badInput === true);
17
+ } else {
18
+ $this.siblings('label').removeClass('active');
19
+ }
20
+ });
21
+ };
22
+
23
+ M.validate_field = function(object) {
24
+ let hasLength = object.attr('data-length') !== null;
25
+ let lenAttr = parseInt(object.attr('data-length'));
26
+ let len = object[0].value.length;
27
+
28
+ if (len === 0 && object[0].validity.badInput === false && !object.is(':required')) {
29
+ if (object.hasClass('validate')) {
30
+ object.removeClass('valid');
31
+ object.removeClass('invalid');
32
+ }
33
+ } else {
34
+ if (object.hasClass('validate')) {
35
+ // Check for character counter attributes
36
+ if (
37
+ (object.is(':valid') && hasLength && len <= lenAttr) ||
38
+ (object.is(':valid') && !hasLength)
39
+ ) {
40
+ object.removeClass('invalid');
41
+ object.addClass('valid');
42
+ } else {
43
+ object.removeClass('valid');
44
+ object.addClass('invalid');
45
+ }
46
+ }
47
+ }
48
+ };
49
+
50
+ M.textareaAutoResize = function($textarea) {
51
+ // Wrap if native element
52
+ if ($textarea instanceof Element) {
53
+ $textarea = $($textarea);
54
+ }
55
+
56
+ if (!$textarea.length) {
57
+ console.error('No textarea element found');
58
+ return;
59
+ }
60
+
61
+ // Textarea Auto Resize
62
+ let hiddenDiv = $('.hiddendiv').first();
63
+ if (!hiddenDiv.length) {
64
+ hiddenDiv = $('<div class="hiddendiv common"></div>');
65
+ $('body').append(hiddenDiv);
66
+ }
67
+
68
+ // Set font properties of hiddenDiv
69
+ let fontFamily = $textarea.css('font-family');
70
+ let fontSize = $textarea.css('font-size');
71
+ let lineHeight = $textarea.css('line-height');
72
+
73
+ // Firefox can't handle padding shorthand.
74
+ let paddingTop = $textarea.css('padding-top');
75
+ let paddingRight = $textarea.css('padding-right');
76
+ let paddingBottom = $textarea.css('padding-bottom');
77
+ let paddingLeft = $textarea.css('padding-left');
78
+
79
+ if (fontSize) {
80
+ hiddenDiv.css('font-size', fontSize);
81
+ }
82
+ if (fontFamily) {
83
+ hiddenDiv.css('font-family', fontFamily);
84
+ }
85
+ if (lineHeight) {
86
+ hiddenDiv.css('line-height', lineHeight);
87
+ }
88
+ if (paddingTop) {
89
+ hiddenDiv.css('padding-top', paddingTop);
90
+ }
91
+ if (paddingRight) {
92
+ hiddenDiv.css('padding-right', paddingRight);
93
+ }
94
+ if (paddingBottom) {
95
+ hiddenDiv.css('padding-bottom', paddingBottom);
96
+ }
97
+ if (paddingLeft) {
98
+ hiddenDiv.css('padding-left', paddingLeft);
99
+ }
100
+
101
+ // Set original-height, if none
102
+ if (!$textarea.data('original-height')) {
103
+ $textarea.data('original-height', $textarea.height());
104
+ }
105
+
106
+ if ($textarea.attr('wrap') === 'off') {
107
+ hiddenDiv.css('overflow-wrap', 'normal').css('white-space', 'pre');
108
+ }
109
+
110
+ hiddenDiv.text($textarea[0].value + '\n');
111
+ let content = hiddenDiv.html().replace(/\n/g, '<br>');
112
+ hiddenDiv.html(content);
113
+
114
+ // When textarea is hidden, width goes crazy.
115
+ // Approximate with half of window size
116
+
117
+ if ($textarea[0].offsetWidth > 0 && $textarea[0].offsetHeight > 0) {
118
+ hiddenDiv.css('width', $textarea.width() + 'px');
119
+ } else {
120
+ hiddenDiv.css('width', window.innerWidth / 2 + 'px');
121
+ }
122
+
123
+ /**
124
+ * Resize if the new height is greater than the
125
+ * original height of the textarea
126
+ */
127
+ if ($textarea.data('original-height') <= hiddenDiv.innerHeight()) {
128
+ $textarea.css('height', hiddenDiv.innerHeight() + 'px');
129
+ } else if ($textarea[0].value.length < $textarea.data('previous-length')) {
130
+ /**
131
+ * In case the new height is less than original height, it
132
+ * means the textarea has less text than before
133
+ * So we set the height to the original one
134
+ */
135
+ $textarea.css('height', $textarea.data('original-height') + 'px');
136
+ }
137
+ $textarea.data('previous-length', $textarea[0].value.length);
138
+ };
139
+
140
+ $(document).ready(function() {
141
+ // Text based inputs
142
+ let input_selector =
143
+ 'input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], input[type=number], input[type=search], input[type=date], input[type=time], textarea';
144
+
145
+ // Add active if form auto complete
146
+ $(document).on('change', input_selector, function() {
147
+ if (this.value.length !== 0 || $(this).attr('placeholder') !== null) {
148
+ $(this)
149
+ .siblings('label')
150
+ .addClass('active');
151
+ }
152
+ M.validate_field($(this));
153
+ });
154
+
155
+ // Add active if input element has been pre-populated on document ready
156
+ $(document).ready(function() {
157
+ M.updateTextFields();
158
+ });
159
+
160
+ // HTML DOM FORM RESET handling
161
+ $(document).on('reset', function(e) {
162
+ let formReset = $(e.target);
163
+ if (formReset.is('form')) {
164
+ formReset
165
+ .find(input_selector)
166
+ .removeClass('valid')
167
+ .removeClass('invalid');
168
+ formReset.find(input_selector).each(function(e) {
169
+ if (this.value.length) {
170
+ $(this)
171
+ .siblings('label')
172
+ .removeClass('active');
173
+ }
174
+ });
175
+
176
+ // Reset select (after native reset)
177
+ setTimeout(function() {
178
+ formReset.find('select').each(function() {
179
+ // check if initialized
180
+ if (this.M_FormSelect) {
181
+ $(this).trigger('change');
182
+ }
183
+ });
184
+ }, 0);
185
+ }
186
+ });
187
+
188
+ /**
189
+ * Add active when element has focus
190
+ * @param {Event} e
191
+ */
192
+ document.addEventListener(
193
+ 'focus',
194
+ function(e) {
195
+ if ($(e.target).is(input_selector)) {
196
+ $(e.target)
197
+ .siblings('label, .prefix')
198
+ .addClass('active');
199
+ }
200
+ },
201
+ true
202
+ );
203
+
204
+ /**
205
+ * Remove active when element is blurred
206
+ * @param {Event} e
207
+ */
208
+ document.addEventListener(
209
+ 'blur',
210
+ function(e) {
211
+ let $inputElement = $(e.target);
212
+ if ($inputElement.is(input_selector)) {
213
+ let selector = '.prefix';
214
+
215
+ if (
216
+ $inputElement[0].value.length === 0 &&
217
+ $inputElement[0].validity.badInput !== true &&
218
+ $inputElement.attr('placeholder') === null
219
+ ) {
220
+ selector += ', label';
221
+ }
222
+ $inputElement.siblings(selector).removeClass('active');
223
+ M.validate_field($inputElement);
224
+ }
225
+ },
226
+ true
227
+ );
228
+
229
+ // Radio and Checkbox focus class
230
+ let radio_checkbox = 'input[type=radio], input[type=checkbox]';
231
+ $(document).on('keyup', radio_checkbox, function(e) {
232
+ // TAB, check if tabbing to radio or checkbox.
233
+ if (e.which === M.keys.TAB) {
234
+ $(this).addClass('tabbed');
235
+ let $this = $(this);
236
+ $this.one('blur', function(e) {
237
+ $(this).removeClass('tabbed');
238
+ });
239
+ return;
240
+ }
241
+ });
242
+
243
+ let text_area_selector = '.materialize-textarea';
244
+ $(text_area_selector).each(function() {
245
+ let $textarea = $(this);
246
+ /**
247
+ * Resize textarea on document load after storing
248
+ * the original height and the original length
249
+ */
250
+ $textarea.data('original-height', $textarea.height());
251
+ $textarea.data('previous-length', this.value.length);
252
+ M.textareaAutoResize($textarea);
253
+ });
254
+
255
+ $(document).on('keyup', text_area_selector, function() {
256
+ M.textareaAutoResize($(this));
257
+ });
258
+ $(document).on('keydown', text_area_selector, function() {
259
+ M.textareaAutoResize($(this));
260
+ });
261
+
262
+ // File Input Path
263
+ $(document).on('change', '.file-field input[type="file"]', function() {
264
+ let file_field = $(this).closest('.file-field');
265
+ let path_input = file_field.find('input.file-path');
266
+ let files = $(this)[0].files;
267
+ let file_names = [];
268
+ for (let i = 0; i < files.length; i++) {
269
+ file_names.push(files[i].name);
270
+ }
271
+ path_input[0].value = file_names.join(', ');
272
+ path_input.trigger('change');
273
+ });
274
+ }); // End of $(document).ready
275
+ })(cash);
package/js/global.js ADDED
@@ -0,0 +1,444 @@
1
+ // Required for Meteor package, the use of window prevents export by Meteor
2
+ (function(window) {
3
+ if (window.Package) {
4
+ M = {};
5
+ } else {
6
+ window.M = {};
7
+ }
8
+
9
+ // Check for jQuery
10
+ M.jQueryLoaded = !!window.jQuery;
11
+ })(window);
12
+
13
+ // AMD
14
+ if (typeof define === 'function' && define.amd) {
15
+ define('M', [], function() {
16
+ return M;
17
+ });
18
+
19
+ // Common JS
20
+ } else if (typeof exports !== 'undefined' && !exports.nodeType) {
21
+ if (typeof module !== 'undefined' && !module.nodeType && module.exports) {
22
+ exports = module.exports = M;
23
+ }
24
+ exports.default = M;
25
+ }
26
+
27
+ M.version = '1.0.0';
28
+
29
+ M.keys = {
30
+ TAB: 9,
31
+ ENTER: 13,
32
+ ESC: 27,
33
+ ARROW_UP: 38,
34
+ ARROW_DOWN: 40
35
+ };
36
+
37
+ /**
38
+ * TabPress Keydown handler
39
+ */
40
+ M.tabPressed = false;
41
+ M.keyDown = false;
42
+ let docHandleKeydown = function(e) {
43
+ M.keyDown = true;
44
+ if (e.which === M.keys.TAB || e.which === M.keys.ARROW_DOWN || e.which === M.keys.ARROW_UP) {
45
+ M.tabPressed = true;
46
+ }
47
+ };
48
+ let docHandleKeyup = function(e) {
49
+ M.keyDown = false;
50
+ if (e.which === M.keys.TAB || e.which === M.keys.ARROW_DOWN || e.which === M.keys.ARROW_UP) {
51
+ M.tabPressed = false;
52
+ }
53
+ };
54
+ let docHandleFocus = function(e) {
55
+ if (M.keyDown) {
56
+ document.body.classList.add('keyboard-focused');
57
+ }
58
+ };
59
+ let docHandleBlur = function(e) {
60
+ document.body.classList.remove('keyboard-focused');
61
+ };
62
+ document.addEventListener('keydown', docHandleKeydown, true);
63
+ document.addEventListener('keyup', docHandleKeyup, true);
64
+ document.addEventListener('focus', docHandleFocus, true);
65
+ document.addEventListener('blur', docHandleBlur, true);
66
+
67
+ /**
68
+ * Initialize jQuery wrapper for plugin
69
+ * @param {Class} plugin javascript class
70
+ * @param {string} pluginName jQuery plugin name
71
+ * @param {string} classRef Class reference name
72
+ */
73
+ M.initializeJqueryWrapper = function(plugin, pluginName, classRef) {
74
+ jQuery.fn[pluginName] = function(methodOrOptions) {
75
+ // Call plugin method if valid method name is passed in
76
+ if (plugin.prototype[methodOrOptions]) {
77
+ let params = Array.prototype.slice.call(arguments, 1);
78
+
79
+ // Getter methods
80
+ if (methodOrOptions.slice(0, 3) === 'get') {
81
+ let instance = this.first()[0][classRef];
82
+ return instance[methodOrOptions].apply(instance, params);
83
+ }
84
+
85
+ // Void methods
86
+ return this.each(function() {
87
+ let instance = this[classRef];
88
+ instance[methodOrOptions].apply(instance, params);
89
+ });
90
+
91
+ // Initialize plugin if options or no argument is passed in
92
+ } else if (typeof methodOrOptions === 'object' || !methodOrOptions) {
93
+ plugin.init(this, arguments[0]);
94
+ return this;
95
+ }
96
+
97
+ // Return error if an unrecognized method name is passed in
98
+ jQuery.error(`Method ${methodOrOptions} does not exist on jQuery.${pluginName}`);
99
+ };
100
+ };
101
+
102
+ /**
103
+ * Automatically initialize components
104
+ * @param {Element} context DOM Element to search within for components
105
+ */
106
+ M.AutoInit = function(context) {
107
+ // Use document.body if no context is given
108
+ let root = !!context ? context : document.body;
109
+
110
+ let registry = {
111
+ Autocomplete: root.querySelectorAll('.autocomplete:not(.no-autoinit)'),
112
+ Carousel: root.querySelectorAll('.carousel:not(.no-autoinit)'),
113
+ Chips: root.querySelectorAll('.chips:not(.no-autoinit)'),
114
+ Collapsible: root.querySelectorAll('.collapsible:not(.no-autoinit)'),
115
+ Datepicker: root.querySelectorAll('.datepicker:not(.no-autoinit)'),
116
+ Dropdown: root.querySelectorAll('.dropdown-trigger:not(.no-autoinit)'),
117
+ Materialbox: root.querySelectorAll('.materialboxed:not(.no-autoinit)'),
118
+ Modal: root.querySelectorAll('.modal:not(.no-autoinit)'),
119
+ Parallax: root.querySelectorAll('.parallax:not(.no-autoinit)'),
120
+ Pushpin: root.querySelectorAll('.pushpin:not(.no-autoinit)'),
121
+ ScrollSpy: root.querySelectorAll('.scrollspy:not(.no-autoinit)'),
122
+ FormSelect: root.querySelectorAll('select:not(.no-autoinit)'),
123
+ Sidenav: root.querySelectorAll('.sidenav:not(.no-autoinit)'),
124
+ Tabs: root.querySelectorAll('.tabs:not(.no-autoinit)'),
125
+ TapTarget: root.querySelectorAll('.tap-target:not(.no-autoinit)'),
126
+ Timepicker: root.querySelectorAll('.timepicker:not(.no-autoinit)'),
127
+ Tooltip: root.querySelectorAll('.tooltipped:not(.no-autoinit)'),
128
+ FloatingActionButton: root.querySelectorAll('.fixed-action-btn:not(.no-autoinit)')
129
+ };
130
+
131
+ for (let pluginName in registry) {
132
+ let plugin = M[pluginName];
133
+ plugin.init(registry[pluginName]);
134
+ }
135
+ };
136
+
137
+ /**
138
+ * Generate approximated selector string for a jQuery object
139
+ * @param {jQuery} obj jQuery object to be parsed
140
+ * @returns {string}
141
+ */
142
+ M.objectSelectorString = function(obj) {
143
+ let tagStr = obj.prop('tagName') || '';
144
+ let idStr = obj.attr('id') || '';
145
+ let classStr = obj.attr('class') || '';
146
+ return (tagStr + idStr + classStr).replace(/\s/g, '');
147
+ };
148
+
149
+ // Unique Random ID
150
+ M.guid = (function() {
151
+ function s4() {
152
+ return Math.floor((1 + Math.random()) * 0x10000)
153
+ .toString(16)
154
+ .substring(1);
155
+ }
156
+ return function() {
157
+ return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
158
+ };
159
+ })();
160
+
161
+ /**
162
+ * Escapes hash from special characters
163
+ * @param {string} hash String returned from this.hash
164
+ * @returns {string}
165
+ */
166
+ M.escapeHash = function(hash) {
167
+ return hash.replace(/(:|\.|\[|\]|,|=|\/)/g, '\\$1');
168
+ };
169
+
170
+ /**
171
+ * Get closest ancestor that satisfies the condition
172
+ * @param {Element} el Element to find ancestors on
173
+ * @param {Function} condition Function that given an ancestor element returns true or false
174
+ * @returns {Element} Return closest ancestor or null if none satisfies the condition
175
+ */
176
+ M.getClosestAncestor = function(el, condition) {
177
+ let ancestor = el.parentNode;
178
+ while (ancestor !== null && !$(ancestor).is(document)) {
179
+ if (condition(ancestor)) {
180
+ return ancestor;
181
+ }
182
+ ancestor = ancestor.parentNode;
183
+ }
184
+ return null;
185
+ };
186
+
187
+ M.elementOrParentIsFixed = function(element) {
188
+ let $element = $(element);
189
+ let $checkElements = $element.add($element.parents());
190
+ let isFixed = false;
191
+ $checkElements.each(function() {
192
+ if ($(this).css('position') === 'fixed') {
193
+ isFixed = true;
194
+ return false;
195
+ }
196
+ });
197
+ return isFixed;
198
+ };
199
+
200
+ /**
201
+ * @typedef {Object} Edges
202
+ * @property {Boolean} top If the top edge was exceeded
203
+ * @property {Boolean} right If the right edge was exceeded
204
+ * @property {Boolean} bottom If the bottom edge was exceeded
205
+ * @property {Boolean} left If the left edge was exceeded
206
+ */
207
+
208
+ /**
209
+ * @typedef {Object} Bounding
210
+ * @property {Number} left left offset coordinate
211
+ * @property {Number} top top offset coordinate
212
+ * @property {Number} width
213
+ * @property {Number} height
214
+ */
215
+
216
+ /**
217
+ * Escapes hash from special characters
218
+ * @param {Element} container Container element that acts as the boundary
219
+ * @param {Bounding} bounding element bounding that is being checked
220
+ * @param {Number} offset offset from edge that counts as exceeding
221
+ * @returns {Edges}
222
+ */
223
+ M.checkWithinContainer = function(container, bounding, offset) {
224
+ let edges = {
225
+ top: false,
226
+ right: false,
227
+ bottom: false,
228
+ left: false
229
+ };
230
+
231
+ let containerRect = container.getBoundingClientRect();
232
+ // If body element is smaller than viewport, use viewport height instead.
233
+ let containerBottom =
234
+ container === document.body
235
+ ? Math.max(containerRect.bottom, window.innerHeight)
236
+ : containerRect.bottom;
237
+
238
+ let scrollLeft = container.scrollLeft;
239
+ let scrollTop = container.scrollTop;
240
+
241
+ let scrolledX = bounding.left - scrollLeft;
242
+ let scrolledY = bounding.top - scrollTop;
243
+
244
+ // Check for container and viewport for each edge
245
+ if (scrolledX < containerRect.left + offset || scrolledX < offset) {
246
+ edges.left = true;
247
+ }
248
+
249
+ if (
250
+ scrolledX + bounding.width > containerRect.right - offset ||
251
+ scrolledX + bounding.width > window.innerWidth - offset
252
+ ) {
253
+ edges.right = true;
254
+ }
255
+
256
+ if (scrolledY < containerRect.top + offset || scrolledY < offset) {
257
+ edges.top = true;
258
+ }
259
+
260
+ if (
261
+ scrolledY + bounding.height > containerBottom - offset ||
262
+ scrolledY + bounding.height > window.innerHeight - offset
263
+ ) {
264
+ edges.bottom = true;
265
+ }
266
+
267
+ return edges;
268
+ };
269
+
270
+ M.checkPossibleAlignments = function(el, container, bounding, offset) {
271
+ let canAlign = {
272
+ top: true,
273
+ right: true,
274
+ bottom: true,
275
+ left: true,
276
+ spaceOnTop: null,
277
+ spaceOnRight: null,
278
+ spaceOnBottom: null,
279
+ spaceOnLeft: null
280
+ };
281
+
282
+ let containerAllowsOverflow = getComputedStyle(container).overflow === 'visible';
283
+ let containerRect = container.getBoundingClientRect();
284
+ let containerHeight = Math.min(containerRect.height, window.innerHeight);
285
+ let containerWidth = Math.min(containerRect.width, window.innerWidth);
286
+ let elOffsetRect = el.getBoundingClientRect();
287
+
288
+ let scrollLeft = container.scrollLeft;
289
+ let scrollTop = container.scrollTop;
290
+
291
+ let scrolledX = bounding.left - scrollLeft;
292
+ let scrolledYTopEdge = bounding.top - scrollTop;
293
+ let scrolledYBottomEdge = bounding.top + elOffsetRect.height - scrollTop;
294
+
295
+ // Check for container and viewport for left
296
+ canAlign.spaceOnRight = !containerAllowsOverflow
297
+ ? containerWidth - (scrolledX + bounding.width)
298
+ : window.innerWidth - (elOffsetRect.left + bounding.width);
299
+ if (canAlign.spaceOnRight < 0) {
300
+ canAlign.left = false;
301
+ }
302
+
303
+ // Check for container and viewport for Right
304
+ canAlign.spaceOnLeft = !containerAllowsOverflow
305
+ ? scrolledX - bounding.width + elOffsetRect.width
306
+ : elOffsetRect.right - bounding.width;
307
+ if (canAlign.spaceOnLeft < 0) {
308
+ canAlign.right = false;
309
+ }
310
+
311
+ // Check for container and viewport for Top
312
+ canAlign.spaceOnBottom = !containerAllowsOverflow
313
+ ? containerHeight - (scrolledYTopEdge + bounding.height + offset)
314
+ : window.innerHeight - (elOffsetRect.top + bounding.height + offset);
315
+ if (canAlign.spaceOnBottom < 0) {
316
+ canAlign.top = false;
317
+ }
318
+
319
+ // Check for container and viewport for Bottom
320
+ canAlign.spaceOnTop = !containerAllowsOverflow
321
+ ? scrolledYBottomEdge - (bounding.height - offset)
322
+ : elOffsetRect.bottom - (bounding.height + offset);
323
+ if (canAlign.spaceOnTop < 0) {
324
+ canAlign.bottom = false;
325
+ }
326
+
327
+ return canAlign;
328
+ };
329
+
330
+ M.getOverflowParent = function(element) {
331
+ if (element == null) {
332
+ return null;
333
+ }
334
+
335
+ if (element === document.body || getComputedStyle(element).overflow !== 'visible') {
336
+ return element;
337
+ }
338
+
339
+ return M.getOverflowParent(element.parentElement);
340
+ };
341
+
342
+ /**
343
+ * Gets id of component from a trigger
344
+ * @param {Element} trigger trigger
345
+ * @returns {string}
346
+ */
347
+ M.getIdFromTrigger = function(trigger) {
348
+ let id = trigger.getAttribute('data-target');
349
+ if (!id) {
350
+ id = trigger.getAttribute('href');
351
+ if (id) {
352
+ id = id.slice(1);
353
+ } else {
354
+ id = '';
355
+ }
356
+ }
357
+ return id;
358
+ };
359
+
360
+ /**
361
+ * Multi browser support for document scroll top
362
+ * @returns {Number}
363
+ */
364
+ M.getDocumentScrollTop = function() {
365
+ return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
366
+ };
367
+
368
+ /**
369
+ * Multi browser support for document scroll left
370
+ * @returns {Number}
371
+ */
372
+ M.getDocumentScrollLeft = function() {
373
+ return window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
374
+ };
375
+
376
+ /**
377
+ * @typedef {Object} Edges
378
+ * @property {Boolean} top If the top edge was exceeded
379
+ * @property {Boolean} right If the right edge was exceeded
380
+ * @property {Boolean} bottom If the bottom edge was exceeded
381
+ * @property {Boolean} left If the left edge was exceeded
382
+ */
383
+
384
+ /**
385
+ * @typedef {Object} Bounding
386
+ * @property {Number} left left offset coordinate
387
+ * @property {Number} top top offset coordinate
388
+ * @property {Number} width
389
+ * @property {Number} height
390
+ */
391
+
392
+ /**
393
+ * Get time in ms
394
+ * @license https://raw.github.com/jashkenas/underscore/master/LICENSE
395
+ * @type {function}
396
+ * @return {number}
397
+ */
398
+ let getTime =
399
+ Date.now ||
400
+ function() {
401
+ return new Date().getTime();
402
+ };
403
+
404
+ /**
405
+ * Returns a function, that, when invoked, will only be triggered at most once
406
+ * during a given window of time. Normally, the throttled function will run
407
+ * as much as it can, without ever going more than once per `wait` duration;
408
+ * but if you'd like to disable the execution on the leading edge, pass
409
+ * `{leading: false}`. To disable execution on the trailing edge, ditto.
410
+ * @license https://raw.github.com/jashkenas/underscore/master/LICENSE
411
+ * @param {function} func
412
+ * @param {number} wait
413
+ * @param {Object=} options
414
+ * @returns {Function}
415
+ */
416
+ M.throttle = function(func, wait, options) {
417
+ let context, args, result;
418
+ let timeout = null;
419
+ let previous = 0;
420
+ options || (options = {});
421
+ let later = function() {
422
+ previous = options.leading === false ? 0 : getTime();
423
+ timeout = null;
424
+ result = func.apply(context, args);
425
+ context = args = null;
426
+ };
427
+ return function() {
428
+ let now = getTime();
429
+ if (!previous && options.leading === false) previous = now;
430
+ let remaining = wait - (now - previous);
431
+ context = this;
432
+ args = arguments;
433
+ if (remaining <= 0) {
434
+ clearTimeout(timeout);
435
+ timeout = null;
436
+ previous = now;
437
+ result = func.apply(context, args);
438
+ context = args = null;
439
+ } else if (!timeout && options.trailing !== false) {
440
+ timeout = setTimeout(later, remaining);
441
+ }
442
+ return result;
443
+ };
444
+ };