mtrl 0.2.3 → 0.2.5
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/package.json +6 -3
- package/src/components/slider/_styles.scss +72 -154
- package/src/components/slider/api.ts +36 -101
- package/src/components/slider/config.ts +26 -73
- package/src/components/slider/constants.ts +12 -8
- package/src/components/slider/features/appearance.ts +1 -47
- package/src/components/slider/features/interactions.ts +14 -9
- package/src/components/slider/features/keyboard.ts +0 -2
- package/src/components/slider/features/structure.ts +151 -191
- package/src/components/slider/features/ui.ts +222 -301
- package/src/components/slider/index.ts +11 -1
- package/src/components/slider/slider.ts +1 -1
- package/src/components/slider/types.ts +10 -25
- package/src/core/build/_ripple.scss +79 -0
- package/src/core/build/constants.ts +48 -0
- package/src/core/build/icon.ts +137 -0
- package/src/core/build/ripple.ts +216 -0
- package/src/core/build/text.ts +91 -0
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
// src/components/slider/index.ts
|
|
2
|
+
|
|
3
|
+
// Export main component creator
|
|
2
4
|
export { default } from './slider';
|
|
5
|
+
|
|
6
|
+
// Export constants
|
|
3
7
|
export {
|
|
4
8
|
SLIDER_COLORS,
|
|
5
9
|
SLIDER_SIZES,
|
|
6
10
|
SLIDER_ORIENTATIONS,
|
|
7
11
|
SLIDER_EVENTS
|
|
8
12
|
} from './constants';
|
|
9
|
-
|
|
13
|
+
|
|
14
|
+
// Export types for TypeScript users
|
|
15
|
+
export type {
|
|
16
|
+
SliderConfig,
|
|
17
|
+
SliderComponent,
|
|
18
|
+
SliderEvent
|
|
19
|
+
} from './types';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/slider/slider.ts
|
|
2
|
-
import { pipe } from '../../core/compose';
|
|
2
|
+
import { pipe } from '../../core/compose/pipe';
|
|
3
3
|
import { createBase, withElement } from '../../core/compose/component';
|
|
4
4
|
import { withEvents, withLifecycle } from '../../core/compose/features';
|
|
5
5
|
import {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/slider/types.ts
|
|
2
|
-
import { SLIDER_COLORS, SLIDER_SIZES,
|
|
2
|
+
import { SLIDER_COLORS, SLIDER_SIZES, SLIDER_EVENTS } from './constants';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Configuration interface for the Slider component
|
|
@@ -24,26 +24,20 @@ export interface SliderConfig {
|
|
|
24
24
|
disabled?: boolean;
|
|
25
25
|
|
|
26
26
|
/** Color variant of the slider */
|
|
27
|
-
color?: keyof typeof SLIDER_COLORS | SLIDER_COLORS;
|
|
27
|
+
color?: keyof typeof SLIDER_COLORS | typeof SLIDER_COLORS[keyof typeof SLIDER_COLORS];
|
|
28
28
|
|
|
29
29
|
/** Size variant of the slider */
|
|
30
|
-
size?: keyof typeof SLIDER_SIZES | SLIDER_SIZES;
|
|
31
|
-
|
|
32
|
-
/** Orientation of the slider */
|
|
33
|
-
orientation?: keyof typeof SLIDER_ORIENTATIONS | SLIDER_ORIENTATIONS;
|
|
30
|
+
size?: keyof typeof SLIDER_SIZES | typeof SLIDER_SIZES[keyof typeof SLIDER_SIZES];
|
|
34
31
|
|
|
35
32
|
/** Whether to show tick marks */
|
|
36
33
|
ticks?: boolean;
|
|
37
34
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
35
|
+
/** Format function for displayed values */
|
|
36
|
+
valueFormatter?: (value: number) => string;
|
|
40
37
|
|
|
41
38
|
/** Whether to show the current value while dragging */
|
|
42
39
|
showValue?: boolean;
|
|
43
40
|
|
|
44
|
-
/** Format function for displayed values */
|
|
45
|
-
valueFormatter?: (value: number) => string;
|
|
46
|
-
|
|
47
41
|
/** Whether to snap to steps while dragging (discrete slider) */
|
|
48
42
|
snapToSteps?: boolean;
|
|
49
43
|
|
|
@@ -55,7 +49,7 @@ export interface SliderConfig {
|
|
|
55
49
|
|
|
56
50
|
/** Event handlers for slider events */
|
|
57
51
|
on?: {
|
|
58
|
-
[key in keyof typeof SLIDER_EVENTS]?: (event: SliderEvent) => void;
|
|
52
|
+
[key in keyof typeof SLIDER_EVENTS | typeof SLIDER_EVENTS[keyof typeof SLIDER_EVENTS]]?: (event: SliderEvent) => void;
|
|
59
53
|
};
|
|
60
54
|
}
|
|
61
55
|
|
|
@@ -129,37 +123,28 @@ export interface SliderComponent {
|
|
|
129
123
|
isDisabled: () => boolean;
|
|
130
124
|
|
|
131
125
|
/** Sets slider color */
|
|
132
|
-
setColor: (color: keyof typeof SLIDER_COLORS | SLIDER_COLORS) => SliderComponent;
|
|
126
|
+
setColor: (color: keyof typeof SLIDER_COLORS | typeof SLIDER_COLORS[keyof typeof SLIDER_COLORS]) => SliderComponent;
|
|
133
127
|
|
|
134
128
|
/** Gets slider color */
|
|
135
129
|
getColor: () => string;
|
|
136
130
|
|
|
137
131
|
/** Sets slider size */
|
|
138
|
-
setSize: (size: keyof typeof SLIDER_SIZES | SLIDER_SIZES) => SliderComponent;
|
|
132
|
+
setSize: (size: keyof typeof SLIDER_SIZES | typeof SLIDER_SIZES[keyof typeof SLIDER_SIZES]) => SliderComponent;
|
|
139
133
|
|
|
140
134
|
/** Gets slider size */
|
|
141
135
|
getSize: () => string;
|
|
142
136
|
|
|
143
|
-
/** Sets slider orientation */
|
|
144
|
-
setOrientation: (orientation: keyof typeof SLIDER_ORIENTATIONS | SLIDER_ORIENTATIONS) => SliderComponent;
|
|
145
|
-
|
|
146
|
-
/** Gets slider orientation */
|
|
147
|
-
getOrientation: () => string;
|
|
148
|
-
|
|
149
137
|
/** Shows or hides tick marks */
|
|
150
138
|
showTicks: (show: boolean) => SliderComponent;
|
|
151
139
|
|
|
152
|
-
/** Shows or hides tick labels */
|
|
153
|
-
showTickLabels: (show: boolean | string[]) => SliderComponent;
|
|
154
|
-
|
|
155
140
|
/** Shows or hides current value while dragging */
|
|
156
141
|
showCurrentValue: (show: boolean) => SliderComponent;
|
|
157
142
|
|
|
158
143
|
/** Adds event listener */
|
|
159
|
-
on: (event: keyof typeof SLIDER_EVENTS | SLIDER_EVENTS, handler: (event: SliderEvent) => void) => SliderComponent;
|
|
144
|
+
on: (event: keyof typeof SLIDER_EVENTS | typeof SLIDER_EVENTS[keyof typeof SLIDER_EVENTS], handler: (event: SliderEvent) => void) => SliderComponent;
|
|
160
145
|
|
|
161
146
|
/** Removes event listener */
|
|
162
|
-
off: (event: keyof typeof SLIDER_EVENTS | SLIDER_EVENTS, handler: (event: SliderEvent) => void) => SliderComponent;
|
|
147
|
+
off: (event: keyof typeof SLIDER_EVENTS | typeof SLIDER_EVENTS[keyof typeof SLIDER_EVENTS], handler: (event: SliderEvent) => void) => SliderComponent;
|
|
163
148
|
|
|
164
149
|
/** Destroys the slider component and cleans up resources */
|
|
165
150
|
destroy: () => void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/components/ripple/_ripple.scss
|
|
2
|
+
@use '../../styles/abstract/base' as base;
|
|
3
|
+
@use '../../styles/abstract/variables' as v;
|
|
4
|
+
@use '../../styles/abstract/functions' as f;
|
|
5
|
+
@use '../../styles/abstract/mixins' as m;
|
|
6
|
+
@use '../../styles/abstract/theme' as t;
|
|
7
|
+
|
|
8
|
+
$component: '#{base.$prefix}-ripple';
|
|
9
|
+
|
|
10
|
+
.#{$component} {
|
|
11
|
+
// Ripple container
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 0;
|
|
14
|
+
left: 0;
|
|
15
|
+
right: 0;
|
|
16
|
+
bottom: 0;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
border-radius: inherit;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
z-index: 0;
|
|
21
|
+
|
|
22
|
+
// Ripple element
|
|
23
|
+
&-wave {
|
|
24
|
+
position: absolute;
|
|
25
|
+
border-radius: 50%;
|
|
26
|
+
background-color: currentColor;
|
|
27
|
+
transform: scale(0);
|
|
28
|
+
opacity: 0;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
will-change: transform, opacity;
|
|
31
|
+
|
|
32
|
+
// Animation
|
|
33
|
+
transition-property: transform, opacity;
|
|
34
|
+
transition-duration: v.motion('duration-short4');
|
|
35
|
+
transition-timing-function: v.motion('easing-standard');
|
|
36
|
+
|
|
37
|
+
// Active ripple
|
|
38
|
+
&.active {
|
|
39
|
+
transform: scale(1);
|
|
40
|
+
opacity: v.state('hover-state-layer-opacity');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&.fade-out {
|
|
44
|
+
opacity: 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Standalone utility for adding ripple to any element
|
|
50
|
+
[data-ripple] {
|
|
51
|
+
position: relative;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
|
|
54
|
+
&::after {
|
|
55
|
+
content: '';
|
|
56
|
+
position: absolute;
|
|
57
|
+
top: 0;
|
|
58
|
+
left: 0;
|
|
59
|
+
right: 0;
|
|
60
|
+
bottom: 0;
|
|
61
|
+
z-index: 0;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle ripple color based on data attribute
|
|
66
|
+
&[data-ripple="light"]::after {
|
|
67
|
+
background-color: rgba(255, 255, 255, 0.3);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&[data-ripple="dark"]::after {
|
|
71
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Make content appear above ripple
|
|
75
|
+
> * {
|
|
76
|
+
position: relative;
|
|
77
|
+
z-index: 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/core/build/constants.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Animation timing functions for ripple effect
|
|
5
|
+
*/
|
|
6
|
+
export enum RIPPLE_TIMING {
|
|
7
|
+
LINEAR = 'linear',
|
|
8
|
+
EASE = 'ease',
|
|
9
|
+
EASE_IN = 'ease-in',
|
|
10
|
+
EASE_OUT = 'ease-out',
|
|
11
|
+
EASE_IN_OUT = 'ease-in-out',
|
|
12
|
+
MATERIAL = 'cubic-bezier(0.4, 0.0, 0.2, 1)'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default configuration for ripple effect
|
|
17
|
+
*/
|
|
18
|
+
export const RIPPLE_CONFIG = {
|
|
19
|
+
duration: 375,
|
|
20
|
+
timing: RIPPLE_TIMING.LINEAR,
|
|
21
|
+
opacity: ['1', '0.3'] as [string, string]
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validation schema for ripple configuration
|
|
26
|
+
*/
|
|
27
|
+
export const RIPPLE_SCHEMA = {
|
|
28
|
+
duration: {
|
|
29
|
+
type: 'number',
|
|
30
|
+
minimum: 0,
|
|
31
|
+
default: RIPPLE_CONFIG.duration
|
|
32
|
+
},
|
|
33
|
+
timing: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: Object.values(RIPPLE_TIMING),
|
|
36
|
+
default: RIPPLE_CONFIG.timing
|
|
37
|
+
},
|
|
38
|
+
opacity: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
pattern: '^[0-1](\\.\\d+)?$'
|
|
43
|
+
},
|
|
44
|
+
minItems: 2,
|
|
45
|
+
maxItems: 2,
|
|
46
|
+
default: RIPPLE_CONFIG.opacity
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/core/build/icon.ts
|
|
2
|
+
/**
|
|
3
|
+
* @module core/build
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for creating an icon element
|
|
8
|
+
*/
|
|
9
|
+
export interface IconElementOptions {
|
|
10
|
+
/**
|
|
11
|
+
* CSS class prefix
|
|
12
|
+
*/
|
|
13
|
+
prefix?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Additional CSS class
|
|
17
|
+
*/
|
|
18
|
+
class?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Icon size variant
|
|
22
|
+
*/
|
|
23
|
+
size?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for icon manager
|
|
28
|
+
*/
|
|
29
|
+
export interface IconConfig {
|
|
30
|
+
/**
|
|
31
|
+
* CSS class prefix
|
|
32
|
+
*/
|
|
33
|
+
prefix?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Component type
|
|
37
|
+
*/
|
|
38
|
+
type?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Icon position ('start' or 'end')
|
|
42
|
+
*/
|
|
43
|
+
position?: 'start' | 'end';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Icon size
|
|
47
|
+
*/
|
|
48
|
+
iconSize?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Icon manager interface
|
|
53
|
+
*/
|
|
54
|
+
export interface IconManager {
|
|
55
|
+
/**
|
|
56
|
+
* Sets icon HTML content
|
|
57
|
+
* @param html - Icon HTML content
|
|
58
|
+
* @returns IconManager instance for chaining
|
|
59
|
+
*/
|
|
60
|
+
setIcon: (html: string) => IconManager;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gets current icon HTML content
|
|
64
|
+
* @returns Current icon HTML
|
|
65
|
+
*/
|
|
66
|
+
getIcon: () => string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Gets icon element
|
|
70
|
+
* @returns Icon element or null if not created
|
|
71
|
+
*/
|
|
72
|
+
getElement: () => HTMLElement | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates an icon DOM element
|
|
77
|
+
*
|
|
78
|
+
* @param html - Icon HTML content
|
|
79
|
+
* @param options - Icon options
|
|
80
|
+
* @returns Icon element
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
const createIconElement = (html: string, options: IconElementOptions = {}): HTMLElement => {
|
|
84
|
+
const PREFIX = options.prefix || 'mtrl';
|
|
85
|
+
const element = document.createElement('span');
|
|
86
|
+
element.className = `${PREFIX}-icon`;
|
|
87
|
+
|
|
88
|
+
if (options.class) {
|
|
89
|
+
element.classList.add(options.class);
|
|
90
|
+
}
|
|
91
|
+
if (options.size) {
|
|
92
|
+
element.classList.add(`${PREFIX}-icon--${options.size}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
element.innerHTML = html;
|
|
96
|
+
return element;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Creates an icon manager for a component
|
|
101
|
+
*
|
|
102
|
+
* @param element - Parent element
|
|
103
|
+
* @param config - Icon configuration
|
|
104
|
+
* @returns Icon manager interface
|
|
105
|
+
*/
|
|
106
|
+
export const createIcon = (element: HTMLElement, config: IconConfig = {}): IconManager => {
|
|
107
|
+
let iconElement: HTMLElement | null = null;
|
|
108
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
setIcon(html: string): IconManager {
|
|
112
|
+
if (!iconElement && html) {
|
|
113
|
+
iconElement = createIconElement(html, {
|
|
114
|
+
prefix: PREFIX,
|
|
115
|
+
class: `${PREFIX}-${config.type || 'component'}-icon`,
|
|
116
|
+
size: config.iconSize
|
|
117
|
+
});
|
|
118
|
+
if (config.position === 'end') {
|
|
119
|
+
element.appendChild(iconElement);
|
|
120
|
+
} else {
|
|
121
|
+
element.insertBefore(iconElement, element.firstChild);
|
|
122
|
+
}
|
|
123
|
+
} else if (iconElement && html) {
|
|
124
|
+
iconElement.innerHTML = html;
|
|
125
|
+
}
|
|
126
|
+
return this;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
getIcon(): string {
|
|
130
|
+
return iconElement ? iconElement.innerHTML : '';
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
getElement(): HTMLElement | null {
|
|
134
|
+
return iconElement;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// src/core/build/ripple.ts
|
|
2
|
+
|
|
3
|
+
import { RIPPLE_CONFIG, RIPPLE_TIMING } from './constants';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ripple animation configuration
|
|
7
|
+
*/
|
|
8
|
+
export interface RippleConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Animation duration in milliseconds
|
|
11
|
+
*/
|
|
12
|
+
duration?: number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Animation timing function
|
|
16
|
+
*/
|
|
17
|
+
timing?: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Opacity start and end values
|
|
21
|
+
*/
|
|
22
|
+
opacity?: [string, string];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* End coordinates for ripple animation
|
|
27
|
+
*/
|
|
28
|
+
interface EndCoordinates {
|
|
29
|
+
size: string;
|
|
30
|
+
top: string;
|
|
31
|
+
left: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Document event listener
|
|
36
|
+
*/
|
|
37
|
+
interface DocumentListener {
|
|
38
|
+
event: string;
|
|
39
|
+
handler: EventListener;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ripple controller interface
|
|
44
|
+
*/
|
|
45
|
+
export interface RippleController {
|
|
46
|
+
/**
|
|
47
|
+
* Attaches ripple effect to an element
|
|
48
|
+
* @param element - Target element
|
|
49
|
+
*/
|
|
50
|
+
mount: (element: HTMLElement) => void;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Removes ripple effect from an element
|
|
54
|
+
* @param element - Target element
|
|
55
|
+
*/
|
|
56
|
+
unmount: (element: HTMLElement) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a ripple effect instance
|
|
61
|
+
*
|
|
62
|
+
* @param config - Ripple configuration
|
|
63
|
+
* @returns Ripple controller instance
|
|
64
|
+
*/
|
|
65
|
+
export const createRipple = (config: RippleConfig = {}): RippleController => {
|
|
66
|
+
// Make sure we fully merge the config options
|
|
67
|
+
const options = {
|
|
68
|
+
...RIPPLE_CONFIG,
|
|
69
|
+
...config,
|
|
70
|
+
// Handle nested objects like opacity array
|
|
71
|
+
opacity: config.opacity || RIPPLE_CONFIG.opacity
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getEndCoordinates = (bounds: DOMRect): EndCoordinates => {
|
|
75
|
+
const size = Math.max(bounds.width, bounds.height);
|
|
76
|
+
const top = bounds.height > bounds.width
|
|
77
|
+
? -bounds.height / 2
|
|
78
|
+
: -(bounds.width - bounds.height / 2);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
size: `${size * 2}px`,
|
|
82
|
+
top: `${top}px`,
|
|
83
|
+
left: `${size / -2}px`
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const createRippleElement = (): HTMLDivElement => {
|
|
88
|
+
const ripple = document.createElement('div');
|
|
89
|
+
ripple.className = 'ripple';
|
|
90
|
+
// Initial styles already set in CSS
|
|
91
|
+
ripple.style.transition = `all ${options.duration}ms ${options.timing}`;
|
|
92
|
+
return ripple;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Store document event listeners for cleanup
|
|
96
|
+
let documentListeners: DocumentListener[] = [];
|
|
97
|
+
|
|
98
|
+
// Safe document event handling
|
|
99
|
+
const addDocumentListener = (event: string, handler: EventListener): void => {
|
|
100
|
+
if (typeof document.addEventListener === 'function') {
|
|
101
|
+
document.addEventListener(event, handler);
|
|
102
|
+
documentListeners.push({ event, handler });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const removeDocumentListener = (event: string, handler: EventListener): void => {
|
|
107
|
+
if (typeof document.removeEventListener === 'function') {
|
|
108
|
+
document.removeEventListener(event, handler);
|
|
109
|
+
documentListeners = documentListeners.filter(
|
|
110
|
+
listener => !(listener.event === event && listener.handler === handler)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const animate = (event: MouseEvent, container: HTMLElement): void => {
|
|
116
|
+
if (!container) return;
|
|
117
|
+
|
|
118
|
+
const bounds = container.getBoundingClientRect();
|
|
119
|
+
const ripple = createRippleElement();
|
|
120
|
+
|
|
121
|
+
// Set initial position and state
|
|
122
|
+
Object.assign(ripple.style, {
|
|
123
|
+
left: `${event.offsetX || bounds.width / 2}px`,
|
|
124
|
+
top: `${event.offsetY || bounds.height / 2}px`,
|
|
125
|
+
transform: 'scale(0)',
|
|
126
|
+
opacity: options.opacity[0]
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
container.appendChild(ripple);
|
|
130
|
+
|
|
131
|
+
// Force reflow
|
|
132
|
+
// eslint-disable-next-line no-unused-expressions
|
|
133
|
+
ripple.offsetHeight;
|
|
134
|
+
|
|
135
|
+
// Animate to end position
|
|
136
|
+
const end = getEndCoordinates(bounds);
|
|
137
|
+
Object.assign(ripple.style, {
|
|
138
|
+
...end,
|
|
139
|
+
transform: 'scale(1)',
|
|
140
|
+
opacity: options.opacity[1]
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const cleanup = () => {
|
|
144
|
+
ripple.style.opacity = '0';
|
|
145
|
+
|
|
146
|
+
// Use setTimeout to remove element after animation
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
if (ripple.parentNode) {
|
|
149
|
+
ripple.parentNode.removeChild(ripple);
|
|
150
|
+
}
|
|
151
|
+
}, options.duration);
|
|
152
|
+
|
|
153
|
+
removeDocumentListener('mouseup', cleanup);
|
|
154
|
+
removeDocumentListener('mouseleave', cleanup);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
addDocumentListener('mouseup', cleanup);
|
|
158
|
+
addDocumentListener('mouseleave', cleanup);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
mount: (element: HTMLElement): void => {
|
|
163
|
+
if (!element) return;
|
|
164
|
+
|
|
165
|
+
// Ensure proper positioning context
|
|
166
|
+
const currentPosition = window.getComputedStyle(element).position;
|
|
167
|
+
if (currentPosition === 'static') {
|
|
168
|
+
element.style.position = 'relative';
|
|
169
|
+
}
|
|
170
|
+
element.style.overflow = 'hidden';
|
|
171
|
+
|
|
172
|
+
// Store the mousedown handler to be able to remove it later
|
|
173
|
+
const mousedownHandler = (e: MouseEvent) => animate(e, element);
|
|
174
|
+
|
|
175
|
+
// Store handler reference on the element
|
|
176
|
+
if (!element.__rippleHandlers) {
|
|
177
|
+
element.__rippleHandlers = [];
|
|
178
|
+
}
|
|
179
|
+
element.__rippleHandlers.push(mousedownHandler);
|
|
180
|
+
|
|
181
|
+
element.addEventListener('mousedown', mousedownHandler);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
unmount: (element: HTMLElement): void => {
|
|
185
|
+
if (!element) return;
|
|
186
|
+
|
|
187
|
+
// Clear document event listeners
|
|
188
|
+
documentListeners.forEach(({ event, handler }) => {
|
|
189
|
+
removeDocumentListener(event, handler);
|
|
190
|
+
});
|
|
191
|
+
documentListeners = [];
|
|
192
|
+
|
|
193
|
+
// Remove event listeners
|
|
194
|
+
if (element.__rippleHandlers) {
|
|
195
|
+
element.__rippleHandlers.forEach(handler => {
|
|
196
|
+
element.removeEventListener('mousedown', handler);
|
|
197
|
+
});
|
|
198
|
+
element.__rippleHandlers = [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Remove all ripple elements
|
|
202
|
+
const ripples = element.querySelectorAll('.ripple');
|
|
203
|
+
ripples.forEach(ripple => {
|
|
204
|
+
// Call remove directly to match the test expectation
|
|
205
|
+
ripple.remove();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Extend the HTMLElement interface to add rippleHandlers property
|
|
212
|
+
declare global {
|
|
213
|
+
interface HTMLElement {
|
|
214
|
+
__rippleHandlers?: Array<(e: MouseEvent) => void>;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/core/build/text.ts
|
|
2
|
+
/**
|
|
3
|
+
* @module core/build
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for text manager
|
|
8
|
+
*/
|
|
9
|
+
export interface TextConfig {
|
|
10
|
+
/**
|
|
11
|
+
* CSS class prefix
|
|
12
|
+
*/
|
|
13
|
+
prefix?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Component type
|
|
17
|
+
*/
|
|
18
|
+
type?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Element to insert before
|
|
22
|
+
*/
|
|
23
|
+
beforeElement?: HTMLElement;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Text manager interface
|
|
28
|
+
*/
|
|
29
|
+
export interface TextManager {
|
|
30
|
+
/**
|
|
31
|
+
* Sets text content
|
|
32
|
+
* @param text - Text content to set
|
|
33
|
+
* @returns TextManager instance for chaining
|
|
34
|
+
*/
|
|
35
|
+
setText: (text: string) => TextManager;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Gets current text content
|
|
39
|
+
* @returns Current text
|
|
40
|
+
*/
|
|
41
|
+
getText: () => string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets text element
|
|
45
|
+
* @returns Text element or null if not created
|
|
46
|
+
*/
|
|
47
|
+
getElement: () => HTMLElement | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a text manager for a component
|
|
52
|
+
*
|
|
53
|
+
* @param element - Parent element
|
|
54
|
+
* @param config - Text configuration
|
|
55
|
+
* @returns Text manager interface
|
|
56
|
+
*/
|
|
57
|
+
export const createText = (element: HTMLElement, config: TextConfig = {}): TextManager => {
|
|
58
|
+
let textElement: HTMLElement | null = null;
|
|
59
|
+
const PREFIX = config.prefix || 'mtrl';
|
|
60
|
+
|
|
61
|
+
const createElement = (content: string): HTMLElement => {
|
|
62
|
+
const span = document.createElement('span');
|
|
63
|
+
span.className = `${PREFIX}-${config.type || 'component'}-text`;
|
|
64
|
+
span.textContent = content;
|
|
65
|
+
return span;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
setText(text: string): TextManager {
|
|
70
|
+
if (!textElement && text) {
|
|
71
|
+
textElement = createElement(text);
|
|
72
|
+
if (config.beforeElement) {
|
|
73
|
+
element.insertBefore(textElement, config.beforeElement);
|
|
74
|
+
} else {
|
|
75
|
+
element.appendChild(textElement);
|
|
76
|
+
}
|
|
77
|
+
} else if (textElement) {
|
|
78
|
+
textElement.textContent = text;
|
|
79
|
+
}
|
|
80
|
+
return this;
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
getText(): string {
|
|
84
|
+
return textElement ? textElement.textContent || '' : '';
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
getElement(): HTMLElement | null {
|
|
88
|
+
return textElement;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
};
|