mtrl 0.8.0-next.43 → 0.8.0-next.45
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/dist/components/menu/features/position.js +2 -0
- package/dist/components/select/features.js +1 -0
- package/dist/components/select/types.d.ts +21 -0
- package/dist/core/compose/cleanup.d.ts +14 -0
- package/dist/core/compose/cleanup.js +51 -0
- package/dist/core/compose/component.d.ts +2 -0
- package/dist/core/compose/component.js +38 -25
- package/dist/core/compose/features/debounce.js +13 -22
- package/dist/core/compose/features/disabled.js +1 -3
- package/dist/core/compose/features/events.js +5 -1
- package/dist/core/compose/features/lifecycle.js +15 -4
- package/dist/core/compose/features/ripple.d.ts +0 -6
- package/dist/core/compose/features/ripple.js +61 -111
- package/dist/core/compose/features/throttle.js +13 -16
- package/dist/core/compose/features/withEvents.js +3 -8
- package/dist/core/dom/create.js +30 -27
- package/dist/core/dom/events.js +29 -15
- package/dist/core/state/events.js +13 -5
- package/dist/core/state/lifecycle.js +6 -1
- package/dist/core/utils/performance.d.ts +5 -2
- package/dist/core/utils/performance.js +16 -2
- package/dist/index.cjs +11 -11
- package/dist/package.json +1 -1
- package/dist/styles/badge.css +1 -1
- package/dist/styles/base.css +2 -2
- package/dist/styles/bottom-app-bar.css +1 -1
- package/dist/styles/bottom-sheet.css +1 -1
- package/dist/styles/button-group.css +1 -1
- package/dist/styles/button.css +1 -1
- package/dist/styles/card.css +1 -1
- package/dist/styles/carousel.css +1 -1
- package/dist/styles/checkbox.css +1 -1
- package/dist/styles/chips.css +1 -1
- package/dist/styles/dialog.css +1 -1
- package/dist/styles/divider.css +1 -1
- package/dist/styles/drawer.css +1 -1
- package/dist/styles/extended-fab.css +1 -1
- package/dist/styles/fab.css +1 -1
- package/dist/styles/icon-button.css +1 -1
- package/dist/styles/list.css +1 -1
- package/dist/styles/loading-indicator.css +1 -1
- package/dist/styles/menu.css +1 -1
- package/dist/styles/navigation-rail.css +1 -1
- package/dist/styles/navigation.css +1 -1
- package/dist/styles/progress.css +1 -1
- package/dist/styles/radios.css +1 -1
- package/dist/styles/search.css +1 -1
- package/dist/styles/segmented-button.css +1 -1
- package/dist/styles/select.css +1 -1
- package/dist/styles/side-sheet.css +1 -1
- package/dist/styles/slider.css +1 -1
- package/dist/styles/snackbar.css +1 -1
- package/dist/styles/split-button.css +1 -1
- package/dist/styles/switch.css +1 -1
- package/dist/styles/tabs.css +1 -1
- package/dist/styles/textfield.css +1 -1
- package/dist/styles/timepicker.css +1 -1
- package/dist/styles/top-app-bar.css +1 -1
- package/dist/styles/utilities.css +1 -1
- package/dist/styles.css +2 -2
- package/dist/themes/autumn.css +1 -1
- package/dist/themes/baseline.css +1 -1
- package/dist/themes/brownbeige.css +1 -1
- package/dist/themes/browngreen.css +1 -1
- package/dist/themes/desert.css +1 -1
- package/dist/themes/forest.css +1 -1
- package/dist/themes/highcontrast.css +1 -1
- package/dist/themes/legacy.css +1 -1
- package/dist/themes/material.css +1 -1
- package/dist/themes/ocean.css +1 -1
- package/dist/themes/sageivory.css +1 -1
- package/dist/themes/spring.css +1 -1
- package/dist/themes/summer.css +1 -1
- package/dist/themes/sunset.css +1 -1
- package/dist/themes/tealcaramel.css +1 -1
- package/dist/themes/winter.css +1 -1
- package/package.json +2 -1
- package/src/styles/utilities/_ripple.scss +12 -3
|
@@ -20,6 +20,8 @@ export const createPositioner = (component, config) => {
|
|
|
20
20
|
tempMenu.style.transform = "none";
|
|
21
21
|
tempMenu.style.opacity = "0";
|
|
22
22
|
tempMenu.style.pointerEvents = "none";
|
|
23
|
+
if (config.maxHeight)
|
|
24
|
+
tempMenu.style.maxHeight = config.maxHeight;
|
|
23
25
|
tempMenu.classList.add(`${component.getClass("menu--visible")}`);
|
|
24
26
|
if (config.width === "100%" && !isSubmenu) {
|
|
25
27
|
tempMenu.style.width = `${openerRect.width}px`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MenuColor, MenuVariant } from "../menu/types.js";
|
|
1
2
|
/**
|
|
2
3
|
* Available Select variants
|
|
3
4
|
*/
|
|
@@ -75,6 +76,26 @@ export interface SelectConfig {
|
|
|
75
76
|
* Whether select is required
|
|
76
77
|
*/
|
|
77
78
|
required?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Options handed to the select's menu. By default the menu lives inside
|
|
81
|
+
* the select's own element with no height of its own, which suits a short
|
|
82
|
+
* list in a still surface; a long list, or a select inside a scrolling or
|
|
83
|
+
* clipping container (a drawer, a sheet), wants the menu in the body with a
|
|
84
|
+
* max height, so it is neither cropped nor taller than the screen.
|
|
85
|
+
* @example { container: document.body, maxHeight: "320px" }
|
|
86
|
+
*/
|
|
87
|
+
menu?: {
|
|
88
|
+
/** Where the menu is mounted and positioned against; document.body for a menu that escapes a clipping container */
|
|
89
|
+
container?: HTMLElement | null;
|
|
90
|
+
/** The menu's max height (a CSS length); the list scrolls inside it */
|
|
91
|
+
maxHeight?: string;
|
|
92
|
+
/** Whether the menu flips above the field when there is no room below */
|
|
93
|
+
autoFlip?: boolean;
|
|
94
|
+
/** The menu's variant: 'vertical' is the M3 expressive menu, items apart in a rounded container */
|
|
95
|
+
variant?: MenuVariant;
|
|
96
|
+
/** The vertical variant's colour mapping */
|
|
97
|
+
color?: MenuColor;
|
|
98
|
+
};
|
|
78
99
|
/**
|
|
79
100
|
* Whether select is disabled
|
|
80
101
|
*/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A resource scope shared by every stage of a component's composition. */
|
|
2
|
+
export interface CleanupScope {
|
|
3
|
+
readonly destroyed: boolean;
|
|
4
|
+
add(cleanup: () => void): void;
|
|
5
|
+
destroy(): void;
|
|
6
|
+
}
|
|
7
|
+
export declare const createCleanup: () => CleanupScope;
|
|
8
|
+
/** Also support custom component bases that already provide a lifecycle. */
|
|
9
|
+
export declare const getCleanup: (component: {
|
|
10
|
+
resources?: CleanupScope;
|
|
11
|
+
lifecycle?: {
|
|
12
|
+
destroy(): void;
|
|
13
|
+
};
|
|
14
|
+
}) => CleanupScope;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const createCleanup = () => {
|
|
2
|
+
let callbacks;
|
|
3
|
+
let destroyed = false;
|
|
4
|
+
return {
|
|
5
|
+
get destroyed() { return destroyed; },
|
|
6
|
+
add(cleanup) {
|
|
7
|
+
if (destroyed)
|
|
8
|
+
cleanup();
|
|
9
|
+
else
|
|
10
|
+
(callbacks ?? (callbacks = [])).push(cleanup);
|
|
11
|
+
},
|
|
12
|
+
destroy() {
|
|
13
|
+
if (destroyed)
|
|
14
|
+
return;
|
|
15
|
+
destroyed = true;
|
|
16
|
+
const pending = callbacks;
|
|
17
|
+
callbacks = undefined;
|
|
18
|
+
let failure;
|
|
19
|
+
let failed = false;
|
|
20
|
+
for (const cleanup of pending ?? []) {
|
|
21
|
+
try {
|
|
22
|
+
cleanup();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (!failed)
|
|
26
|
+
failure = error;
|
|
27
|
+
failed = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (failed)
|
|
31
|
+
throw failure;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export const getCleanup = (component) => {
|
|
36
|
+
if (component.resources)
|
|
37
|
+
return component.resources;
|
|
38
|
+
const resources = component.resources = createCleanup();
|
|
39
|
+
if (component.lifecycle) {
|
|
40
|
+
const destroy = component.lifecycle.destroy.bind(component.lifecycle);
|
|
41
|
+
component.lifecycle.destroy = () => {
|
|
42
|
+
try {
|
|
43
|
+
resources.destroy();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
destroy();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return resources;
|
|
51
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @module core/compose/component
|
|
3
3
|
* @description Core utilities for component composition and creation with built-in mobile support
|
|
4
4
|
*/
|
|
5
|
+
import { type CleanupScope } from "./cleanup.js";
|
|
5
6
|
/**
|
|
6
7
|
* Touch state interface to track touch interactions
|
|
7
8
|
*/
|
|
@@ -18,6 +19,7 @@ export interface TouchState {
|
|
|
18
19
|
* Base component interface with prefix utilities
|
|
19
20
|
*/
|
|
20
21
|
export interface BaseComponent {
|
|
22
|
+
resources?: CleanupScope;
|
|
21
23
|
config: Record<string, any>;
|
|
22
24
|
componentName?: string;
|
|
23
25
|
getClass: (name: string) => string;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { getCleanup } from "./cleanup.js";
|
|
1
2
|
import { createElement, removeEventHandlers, } from "../dom/create.js";
|
|
2
3
|
import { normalizeEvent, hasTouchSupport, TOUCH_CONFIG, PASSIVE_EVENTS, } from "../utils/mobile.js";
|
|
4
|
+
const getModifierClass = (base, modifier) => `${base}--${modifier}`;
|
|
5
|
+
const getElementClass = (base, element) => `${base}-${element}`;
|
|
3
6
|
const withPrefix = (prefix) => ({
|
|
4
7
|
getClass: (name) => `${prefix}-${name}`,
|
|
5
|
-
getModifierClass
|
|
6
|
-
getElementClass
|
|
8
|
+
getModifierClass,
|
|
9
|
+
getElementClass,
|
|
7
10
|
});
|
|
8
11
|
export const createBase = (config = {}) => ({
|
|
9
12
|
config,
|
|
@@ -34,12 +37,12 @@ export const createBase = (config = {}) => ({
|
|
|
34
37
|
}
|
|
35
38
|
},
|
|
36
39
|
});
|
|
37
|
-
|
|
40
|
+
const setupTouch = (element, base, options) => {
|
|
38
41
|
const handleTouchStart = (event) => {
|
|
39
42
|
base.updateTouchState(event, "start");
|
|
40
43
|
element.classList.add(`${base.getClass("touch-active")}`);
|
|
41
|
-
if (options.forwardEvents?.touchstart && "emit" in
|
|
42
|
-
|
|
44
|
+
if (options.forwardEvents?.touchstart && "emit" in base && typeof base.emit === "function") {
|
|
45
|
+
base.emit("touchstart", normalizeEvent(event));
|
|
43
46
|
}
|
|
44
47
|
};
|
|
45
48
|
const handleTouchEnd = (event) => {
|
|
@@ -48,11 +51,11 @@ export const withElement = (options = {}) => (component) => {
|
|
|
48
51
|
const touchDuration = Date.now() - base.touchState.startTime;
|
|
49
52
|
element.classList.remove(`${base.getClass("touch-active")}`);
|
|
50
53
|
base.updateTouchState(event, "end");
|
|
51
|
-
if (touchDuration < TOUCH_CONFIG.TAP_THRESHOLD && "emit" in
|
|
52
|
-
|
|
54
|
+
if (touchDuration < TOUCH_CONFIG.TAP_THRESHOLD && "emit" in base && typeof base.emit === "function") {
|
|
55
|
+
base.emit("tap", normalizeEvent(event));
|
|
53
56
|
}
|
|
54
|
-
if (options.forwardEvents?.touchend && "emit" in
|
|
55
|
-
|
|
57
|
+
if (options.forwardEvents?.touchend && "emit" in base && typeof base.emit === "function") {
|
|
58
|
+
base.emit("touchend", normalizeEvent(event));
|
|
56
59
|
}
|
|
57
60
|
};
|
|
58
61
|
const handleTouchMove = (event) => {
|
|
@@ -62,17 +65,28 @@ export const withElement = (options = {}) => (component) => {
|
|
|
62
65
|
const deltaX = normalized.clientX - base.touchState.startPosition.x;
|
|
63
66
|
const deltaY = normalized.clientY - base.touchState.startPosition.y;
|
|
64
67
|
if (Math.abs(deltaX) > TOUCH_CONFIG.SWIPE_THRESHOLD &&
|
|
65
|
-
"emit" in
|
|
66
|
-
|
|
68
|
+
"emit" in base && typeof base.emit === "function") {
|
|
69
|
+
base.emit("swipe", {
|
|
67
70
|
direction: deltaX > 0 ? "right" : "left",
|
|
68
71
|
deltaX,
|
|
69
72
|
deltaY,
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
|
-
if (options.forwardEvents?.touchmove && "emit" in
|
|
73
|
-
|
|
75
|
+
if (options.forwardEvents?.touchmove && "emit" in base && typeof base.emit === "function") {
|
|
76
|
+
base.emit("touchmove", { ...normalized, deltaX, deltaY });
|
|
74
77
|
}
|
|
75
78
|
};
|
|
79
|
+
element.addEventListener("touchstart", handleTouchStart, PASSIVE_EVENTS);
|
|
80
|
+
element.addEventListener("touchend", handleTouchEnd);
|
|
81
|
+
element.addEventListener("touchmove", handleTouchMove, PASSIVE_EVENTS);
|
|
82
|
+
return () => {
|
|
83
|
+
element.removeEventListener("touchstart", handleTouchStart);
|
|
84
|
+
element.removeEventListener("touchend", handleTouchEnd);
|
|
85
|
+
element.removeEventListener("touchmove", handleTouchMove);
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export const withElement = (options = {}) => (component) => {
|
|
89
|
+
const resources = getCleanup(component);
|
|
76
90
|
const base = component;
|
|
77
91
|
let parent = component.config.parent || options.parent;
|
|
78
92
|
if (typeof parent === "string") {
|
|
@@ -106,11 +120,16 @@ export const withElement = (options = {}) => (component) => {
|
|
|
106
120
|
container: parent,
|
|
107
121
|
};
|
|
108
122
|
const element = createElement(elementOptions);
|
|
109
|
-
|
|
110
|
-
element
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
const cleanupTouch = hasTouchSupport() && options.interactive
|
|
124
|
+
? setupTouch(element, base, options)
|
|
125
|
+
: undefined;
|
|
126
|
+
resources.add(() => {
|
|
127
|
+
cleanupTouch?.();
|
|
128
|
+
base.touchState.activeTarget = null;
|
|
129
|
+
base.touchState.isTouching = false;
|
|
130
|
+
removeEventHandlers(element);
|
|
131
|
+
element.remove();
|
|
132
|
+
});
|
|
114
133
|
return {
|
|
115
134
|
...component,
|
|
116
135
|
element,
|
|
@@ -119,13 +138,7 @@ export const withElement = (options = {}) => (component) => {
|
|
|
119
138
|
return this;
|
|
120
139
|
},
|
|
121
140
|
destroy() {
|
|
122
|
-
|
|
123
|
-
element.removeEventListener("touchstart", handleTouchStart);
|
|
124
|
-
element.removeEventListener("touchend", handleTouchEnd);
|
|
125
|
-
element.removeEventListener("touchmove", handleTouchMove);
|
|
126
|
-
}
|
|
127
|
-
removeEventHandlers(element);
|
|
128
|
-
element.remove();
|
|
141
|
+
resources.destroy();
|
|
129
142
|
},
|
|
130
143
|
};
|
|
131
144
|
};
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
+
import { getCleanup } from "../cleanup.js";
|
|
1
2
|
import { debounce } from "../../utils/performance.js";
|
|
2
|
-
function hasLifecycle(component) {
|
|
3
|
-
return 'lifecycle' in component &&
|
|
4
|
-
component.lifecycle &&
|
|
5
|
-
typeof component.lifecycle === 'object' &&
|
|
6
|
-
'destroy' in component.lifecycle &&
|
|
7
|
-
typeof component.lifecycle.destroy === 'function';
|
|
8
|
-
}
|
|
9
3
|
export const withDebounce = (config = {}) => (component) => {
|
|
10
4
|
const debouncedHandlers = {};
|
|
11
5
|
const addDebouncedEvent = (event, handler, wait, options = {}) => {
|
|
6
|
+
if (resources.destroyed)
|
|
7
|
+
return enhancedComponent;
|
|
12
8
|
if (debouncedHandlers[event]) {
|
|
13
9
|
removeDebouncedEvent(event);
|
|
14
10
|
}
|
|
@@ -23,31 +19,26 @@ export const withDebounce = (config = {}) => (component) => {
|
|
|
23
19
|
const removeDebouncedEvent = (event) => {
|
|
24
20
|
const handler = debouncedHandlers[event];
|
|
25
21
|
if (handler) {
|
|
22
|
+
handler.debounced.cancel();
|
|
26
23
|
component.element.removeEventListener(event, handler.debounced);
|
|
27
24
|
delete debouncedHandlers[event];
|
|
28
25
|
}
|
|
29
26
|
return enhancedComponent;
|
|
30
27
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const handler = debouncedHandlers[event];
|
|
36
|
-
component.element.removeEventListener(event, handler.debounced);
|
|
37
|
-
});
|
|
38
|
-
originalDestroy.call(component.lifecycle);
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
if (config.debouncedEvents) {
|
|
42
|
-
Object.entries(config.debouncedEvents).forEach(([event, settings]) => {
|
|
43
|
-
addDebouncedEvent(event, settings.handler, settings.wait, settings.options);
|
|
44
|
-
});
|
|
45
|
-
}
|
|
28
|
+
const resources = getCleanup(component);
|
|
29
|
+
resources.add(() => {
|
|
30
|
+
Object.keys(debouncedHandlers).forEach(event => removeDebouncedEvent(event));
|
|
31
|
+
});
|
|
46
32
|
const enhancedComponent = {
|
|
47
33
|
...component,
|
|
48
34
|
addDebouncedEvent,
|
|
49
35
|
removeDebouncedEvent
|
|
50
36
|
};
|
|
37
|
+
if (config.debouncedEvents) {
|
|
38
|
+
Object.entries(config.debouncedEvents).forEach(([event, settings]) => {
|
|
39
|
+
addDebouncedEvent(event, settings.handler, settings.wait, settings.options);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
51
42
|
return enhancedComponent;
|
|
52
43
|
};
|
|
53
44
|
export default withDebounce;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { getCleanup } from "../cleanup.js";
|
|
1
2
|
import { createEmitter } from "../../state/emitter.js";
|
|
2
3
|
export const withEvents = () => (component) => {
|
|
3
4
|
const emitter = createEmitter();
|
|
5
|
+
const resources = getCleanup(component);
|
|
6
|
+
resources.add(() => emitter.clear());
|
|
4
7
|
return {
|
|
5
8
|
...component,
|
|
6
9
|
on(event, handler) {
|
|
7
|
-
|
|
10
|
+
if (!resources.destroyed)
|
|
11
|
+
emitter.on(event, handler);
|
|
8
12
|
return this;
|
|
9
13
|
},
|
|
10
14
|
off(event, handler) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getCleanup } from "../cleanup.js";
|
|
1
2
|
import { createEmitter } from "../../state/emitter.js";
|
|
2
3
|
function hasEvents(component) {
|
|
3
4
|
return 'events' in component &&
|
|
@@ -21,13 +22,17 @@ function hasIcon(component) {
|
|
|
21
22
|
typeof component.icon.getElement === 'function';
|
|
22
23
|
}
|
|
23
24
|
export const withLifecycle = () => (component) => {
|
|
25
|
+
const resources = getCleanup(component);
|
|
24
26
|
let mounted = false;
|
|
27
|
+
let destroyed = false;
|
|
28
|
+
const destroyElement = component.destroy?.bind(component);
|
|
25
29
|
const emitter = createEmitter();
|
|
30
|
+
resources.add(() => emitter.clear());
|
|
26
31
|
const lifecycle = {
|
|
27
32
|
onMount: (handler) => emitter.on('mount', handler),
|
|
28
33
|
onUnmount: (handler) => emitter.on('unmount', handler),
|
|
29
34
|
mount: () => {
|
|
30
|
-
if (!mounted) {
|
|
35
|
+
if (!mounted && !destroyed) {
|
|
31
36
|
mounted = true;
|
|
32
37
|
emitter.emit('mount');
|
|
33
38
|
}
|
|
@@ -41,9 +46,14 @@ export const withLifecycle = () => (component) => {
|
|
|
41
46
|
},
|
|
42
47
|
isMounted: () => mounted,
|
|
43
48
|
destroy() {
|
|
49
|
+
if (destroyed)
|
|
50
|
+
return;
|
|
51
|
+
destroyed = true;
|
|
44
52
|
if (mounted) {
|
|
45
53
|
this.unmount();
|
|
46
54
|
}
|
|
55
|
+
emitter.clear();
|
|
56
|
+
resources.destroy();
|
|
47
57
|
if (hasEvents(component)) {
|
|
48
58
|
component.events.destroy();
|
|
49
59
|
}
|
|
@@ -59,9 +69,10 @@ export const withLifecycle = () => (component) => {
|
|
|
59
69
|
iconElement.remove();
|
|
60
70
|
}
|
|
61
71
|
}
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
if (destroyElement)
|
|
73
|
+
destroyElement();
|
|
74
|
+
else
|
|
75
|
+
component.element?.remove();
|
|
65
76
|
}
|
|
66
77
|
};
|
|
67
78
|
return {
|
|
@@ -52,12 +52,6 @@ export interface RippleComponent extends BaseComponent {
|
|
|
52
52
|
* @returns Ripple controller
|
|
53
53
|
*/
|
|
54
54
|
export declare const createRipple: (config?: RippleConfig) => RippleController;
|
|
55
|
-
declare global {
|
|
56
|
-
interface HTMLElement {
|
|
57
|
-
__rippleHandlers?: Array<(e: MouseEvent) => void>;
|
|
58
|
-
__rippleContainer?: HTMLElement;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
55
|
/**
|
|
62
56
|
* Adds ripple effect functionality to a component
|
|
63
57
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RIPPLE_CONFIG } from "./constants.js";
|
|
2
|
+
import { getCleanup } from "../cleanup.js";
|
|
2
3
|
import { PREFIX } from "../../config.js";
|
|
3
4
|
export const createRipple = (config = {}) => {
|
|
4
5
|
const options = {
|
|
@@ -6,111 +7,69 @@ export const createRipple = (config = {}) => {
|
|
|
6
7
|
...config,
|
|
7
8
|
opacity: config.opacity || RIPPLE_CONFIG.opacity,
|
|
8
9
|
};
|
|
9
|
-
const
|
|
10
|
-
let documentListeners = [];
|
|
11
|
-
const createRippleElement = () => {
|
|
12
|
-
const ripple = document.createElement("div");
|
|
13
|
-
ripple.className = `${PREFIX}-ripple-wave`;
|
|
14
|
-
return ripple;
|
|
15
|
-
};
|
|
16
|
-
const addDocumentListener = (event, handler) => {
|
|
17
|
-
if (typeof document.addEventListener === "function") {
|
|
18
|
-
document.addEventListener(event, handler);
|
|
19
|
-
documentListeners.push({ event, handler });
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
const removeDocumentListener = (event, handler) => {
|
|
23
|
-
if (typeof document.removeEventListener === "function") {
|
|
24
|
-
document.removeEventListener(event, handler);
|
|
25
|
-
documentListeners = documentListeners.filter((listener) => !(listener.event === event && listener.handler === handler));
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
const animate = (event, container) => {
|
|
29
|
-
if (!container || !container.__rippleContainer)
|
|
30
|
-
return;
|
|
31
|
-
const rippleContainer = container.__rippleContainer;
|
|
32
|
-
const bounds = container.getBoundingClientRect();
|
|
33
|
-
const ripple = createRippleElement();
|
|
34
|
-
const size = Math.max(bounds.width, bounds.height) * 2;
|
|
35
|
-
const x = event.clientX - bounds.left - size / 2;
|
|
36
|
-
const y = event.clientY - bounds.top - size / 2;
|
|
37
|
-
Object.assign(ripple.style, {
|
|
38
|
-
width: `${size}px`,
|
|
39
|
-
height: `${size}px`,
|
|
40
|
-
left: `${x}px`,
|
|
41
|
-
top: `${y}px`,
|
|
42
|
-
});
|
|
43
|
-
rippleContainer.appendChild(ripple);
|
|
44
|
-
ripple.offsetHeight;
|
|
45
|
-
requestAnimationFrame(() => {
|
|
46
|
-
ripple.classList.add("active");
|
|
47
|
-
});
|
|
48
|
-
const cleanup = () => {
|
|
49
|
-
removeDocumentListener("mouseup", cleanup);
|
|
50
|
-
removeDocumentListener("mouseleave", cleanup);
|
|
51
|
-
ripple.classList.remove("active");
|
|
52
|
-
ripple.classList.add("fade-out");
|
|
53
|
-
setTimeout(() => {
|
|
54
|
-
if (ripple.parentNode) {
|
|
55
|
-
ripple.parentNode.removeChild(ripple);
|
|
56
|
-
}
|
|
57
|
-
activeRipples.get(container)?.delete(ripple);
|
|
58
|
-
}, options.duration);
|
|
59
|
-
};
|
|
60
|
-
addDocumentListener("mouseup", cleanup);
|
|
61
|
-
addDocumentListener("mouseleave", cleanup);
|
|
62
|
-
};
|
|
10
|
+
const mounts = new WeakMap();
|
|
63
11
|
return {
|
|
64
|
-
mount
|
|
65
|
-
if (!element)
|
|
12
|
+
mount(element) {
|
|
13
|
+
if (!element || mounts.has(element))
|
|
66
14
|
return;
|
|
67
|
-
const
|
|
68
|
-
|
|
15
|
+
const doc = element.ownerDocument;
|
|
16
|
+
const view = doc.defaultView;
|
|
17
|
+
if (!view)
|
|
18
|
+
return;
|
|
19
|
+
if (view.getComputedStyle(element).position === "static") {
|
|
69
20
|
element.style.position = "relative";
|
|
70
21
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
element.addEventListener("mousedown", mousedownHandler);
|
|
85
|
-
},
|
|
86
|
-
unmount: (element) => {
|
|
87
|
-
if (!element)
|
|
88
|
-
return;
|
|
89
|
-
documentListeners.forEach(({ event, handler }) => {
|
|
90
|
-
removeDocumentListener(event, handler);
|
|
91
|
-
});
|
|
92
|
-
documentListeners = [];
|
|
93
|
-
if (element.__rippleHandlers) {
|
|
94
|
-
element.__rippleHandlers.forEach((handler) => {
|
|
95
|
-
element.removeEventListener("mousedown", handler);
|
|
22
|
+
const container = doc.createElement("div");
|
|
23
|
+
container.className = `${PREFIX}-ripple`;
|
|
24
|
+
element.appendChild(container);
|
|
25
|
+
const waves = new Set();
|
|
26
|
+
const press = (event) => {
|
|
27
|
+
const bounds = element.getBoundingClientRect();
|
|
28
|
+
const size = Math.max(bounds.width, bounds.height) * 2;
|
|
29
|
+
const wave = doc.createElement("div");
|
|
30
|
+
wave.className = `${PREFIX}-ripple-wave active`;
|
|
31
|
+
Object.assign(wave.style, {
|
|
32
|
+
width: `${size}px`, height: `${size}px`,
|
|
33
|
+
left: `${event.clientX - bounds.left - size / 2}px`,
|
|
34
|
+
top: `${event.clientY - bounds.top - size / 2}px`,
|
|
96
35
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
36
|
+
let timer;
|
|
37
|
+
let released = false;
|
|
38
|
+
const removeListeners = () => {
|
|
39
|
+
doc.removeEventListener("mouseup", release);
|
|
40
|
+
doc.removeEventListener("mouseleave", release);
|
|
41
|
+
};
|
|
42
|
+
const dispose = () => {
|
|
43
|
+
removeListeners();
|
|
44
|
+
if (timer !== undefined)
|
|
45
|
+
view.clearTimeout(timer);
|
|
46
|
+
wave.remove();
|
|
47
|
+
waves.delete(dispose);
|
|
48
|
+
};
|
|
49
|
+
const release = () => {
|
|
50
|
+
if (released)
|
|
51
|
+
return;
|
|
52
|
+
released = true;
|
|
53
|
+
removeListeners();
|
|
54
|
+
wave.classList.add("fade-out");
|
|
55
|
+
timer = view.setTimeout(dispose, options.duration);
|
|
56
|
+
};
|
|
57
|
+
waves.add(dispose);
|
|
58
|
+
doc.addEventListener("mouseup", release);
|
|
59
|
+
doc.addEventListener("mouseleave", release);
|
|
60
|
+
container.appendChild(wave);
|
|
61
|
+
};
|
|
62
|
+
element.addEventListener("mousedown", press);
|
|
63
|
+
mounts.set(element, () => {
|
|
64
|
+
element.removeEventListener("mousedown", press);
|
|
65
|
+
for (const dispose of waves)
|
|
66
|
+
dispose();
|
|
67
|
+
container.remove();
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
unmount(element) {
|
|
71
|
+
mounts.get(element)?.();
|
|
72
|
+
mounts.delete(element);
|
|
114
73
|
},
|
|
115
74
|
};
|
|
116
75
|
};
|
|
@@ -119,17 +78,8 @@ export const withRipple = (config) => (component) => {
|
|
|
119
78
|
return component;
|
|
120
79
|
const rippleInstance = createRipple(config.rippleConfig);
|
|
121
80
|
rippleInstance.mount(component.element);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const originalDestroy = component.lifecycle.destroy;
|
|
125
|
-
component.lifecycle.mount = () => {
|
|
126
|
-
originalMount.call(component.lifecycle);
|
|
127
|
-
};
|
|
128
|
-
component.lifecycle.destroy = () => {
|
|
129
|
-
rippleInstance.unmount(component.element);
|
|
130
|
-
originalDestroy.call(component.lifecycle);
|
|
131
|
-
};
|
|
132
|
-
}
|
|
81
|
+
const resources = getCleanup(component);
|
|
82
|
+
resources.add(() => rippleInstance.unmount(component.element));
|
|
133
83
|
return {
|
|
134
84
|
...component,
|
|
135
85
|
ripple: rippleInstance,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { getCleanup } from "../cleanup.js";
|
|
1
2
|
import { throttle } from "../../utils/performance.js";
|
|
2
|
-
import { hasLifecycle } from "../utils/type-guards.js";
|
|
3
3
|
export const withThrottle = (config = {}) => (component) => {
|
|
4
4
|
const throttledHandlers = {};
|
|
5
5
|
const addThrottledEvent = (event, handler, wait, options = {}) => {
|
|
6
|
+
if (resources.destroyed)
|
|
7
|
+
return enhancedComponent;
|
|
6
8
|
if (throttledHandlers[event]) {
|
|
7
9
|
removeThrottledEvent(event);
|
|
8
10
|
}
|
|
@@ -17,31 +19,26 @@ export const withThrottle = (config = {}) => (component) => {
|
|
|
17
19
|
const removeThrottledEvent = (event) => {
|
|
18
20
|
const handler = throttledHandlers[event];
|
|
19
21
|
if (handler) {
|
|
22
|
+
handler.throttled.cancel();
|
|
20
23
|
component.element.removeEventListener(event, handler.throttled);
|
|
21
24
|
delete throttledHandlers[event];
|
|
22
25
|
}
|
|
23
26
|
return enhancedComponent;
|
|
24
27
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const handler = throttledHandlers[event];
|
|
30
|
-
component.element.removeEventListener(event, handler.throttled);
|
|
31
|
-
});
|
|
32
|
-
originalDestroy.call(component.lifecycle);
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
if (config.throttledEvents) {
|
|
36
|
-
Object.entries(config.throttledEvents).forEach(([event, settings]) => {
|
|
37
|
-
addThrottledEvent(event, settings.handler, settings.wait, settings.options);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
28
|
+
const resources = getCleanup(component);
|
|
29
|
+
resources.add(() => {
|
|
30
|
+
Object.keys(throttledHandlers).forEach(event => removeThrottledEvent(event));
|
|
31
|
+
});
|
|
40
32
|
const enhancedComponent = {
|
|
41
33
|
...component,
|
|
42
34
|
addThrottledEvent,
|
|
43
35
|
removeThrottledEvent,
|
|
44
36
|
};
|
|
37
|
+
if (config.throttledEvents) {
|
|
38
|
+
Object.entries(config.throttledEvents).forEach(([event, settings]) => {
|
|
39
|
+
addThrottledEvent(event, settings.handler, settings.wait, settings.options);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
45
42
|
return enhancedComponent;
|
|
46
43
|
};
|
|
47
44
|
export default withThrottle;
|