mtrl 0.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/LICENSE +21 -0
- package/README.md +251 -0
- package/index.js +10 -0
- package/package.json +17 -0
- package/src/components/button/api.js +54 -0
- package/src/components/button/button.js +81 -0
- package/src/components/button/config.js +8 -0
- package/src/components/button/constants.js +63 -0
- package/src/components/button/index.js +2 -0
- package/src/components/button/styles.scss +231 -0
- package/src/components/checkbox/api.js +45 -0
- package/src/components/checkbox/checkbox.js +95 -0
- package/src/components/checkbox/constants.js +88 -0
- package/src/components/checkbox/index.js +2 -0
- package/src/components/checkbox/styles.scss +183 -0
- package/src/components/container/api.js +42 -0
- package/src/components/container/container.js +45 -0
- package/src/components/container/index.js +2 -0
- package/src/components/container/styles.scss +59 -0
- package/src/components/list/constants.js +89 -0
- package/src/components/list/index.js +2 -0
- package/src/components/list/list-item.js +147 -0
- package/src/components/list/list.js +267 -0
- package/src/components/list/styles/_list-item.scss +142 -0
- package/src/components/list/styles/_list.scss +89 -0
- package/src/components/list/styles/_variables.scss +13 -0
- package/src/components/list/styles.scss +19 -0
- package/src/components/navigation/api.js +43 -0
- package/src/components/navigation/constants.js +235 -0
- package/src/components/navigation/features/items.js +192 -0
- package/src/components/navigation/index.js +2 -0
- package/src/components/navigation/nav-item.js +137 -0
- package/src/components/navigation/navigation.js +55 -0
- package/src/components/navigation/styles/_bar.scss +51 -0
- package/src/components/navigation/styles/_base.scss +129 -0
- package/src/components/navigation/styles/_drawer.scss +169 -0
- package/src/components/navigation/styles/_rail.scss +65 -0
- package/src/components/navigation/styles.scss +6 -0
- package/src/components/snackbar/api.js +125 -0
- package/src/components/snackbar/constants.js +41 -0
- package/src/components/snackbar/features.js +69 -0
- package/src/components/snackbar/index.js +2 -0
- package/src/components/snackbar/position.js +63 -0
- package/src/components/snackbar/queue.js +74 -0
- package/src/components/snackbar/snackbar.js +70 -0
- package/src/components/snackbar/styles.scss +182 -0
- package/src/components/switch/api.js +44 -0
- package/src/components/switch/constants.js +80 -0
- package/src/components/switch/index.js +2 -0
- package/src/components/switch/styles.scss +172 -0
- package/src/components/switch/switch.js +71 -0
- package/src/components/textfield/api.js +49 -0
- package/src/components/textfield/constants.js +81 -0
- package/src/components/textfield/index.js +2 -0
- package/src/components/textfield/styles/base.scss +107 -0
- package/src/components/textfield/styles/filled.scss +58 -0
- package/src/components/textfield/styles/outlined.scss +66 -0
- package/src/components/textfield/styles.scss +6 -0
- package/src/components/textfield/textfield.js +68 -0
- package/src/core/build/constants.js +51 -0
- package/src/core/build/icon.js +78 -0
- package/src/core/build/ripple.js +92 -0
- package/src/core/build/text.js +54 -0
- package/src/core/collection/adapters/base.js +26 -0
- package/src/core/collection/adapters/mongodb.js +232 -0
- package/src/core/collection/adapters/route.js +201 -0
- package/src/core/collection/collection.js +259 -0
- package/src/core/collection/list-manager.js +157 -0
- package/src/core/compose/base.js +8 -0
- package/src/core/compose/component.js +225 -0
- package/src/core/compose/features/checkable.js +114 -0
- package/src/core/compose/features/disabled.js +25 -0
- package/src/core/compose/features/events.js +48 -0
- package/src/core/compose/features/icon.js +33 -0
- package/src/core/compose/features/index.js +20 -0
- package/src/core/compose/features/input.js +92 -0
- package/src/core/compose/features/lifecycle.js +69 -0
- package/src/core/compose/features/position.js +60 -0
- package/src/core/compose/features/ripple.js +32 -0
- package/src/core/compose/features/size.js +9 -0
- package/src/core/compose/features/style.js +12 -0
- package/src/core/compose/features/text.js +17 -0
- package/src/core/compose/features/textinput.js +118 -0
- package/src/core/compose/features/textlabel.js +28 -0
- package/src/core/compose/features/track.js +49 -0
- package/src/core/compose/features/variant.js +9 -0
- package/src/core/compose/features/withEvents.js +67 -0
- package/src/core/compose/index.js +16 -0
- package/src/core/compose/pipe.js +69 -0
- package/src/core/config.js +140 -0
- package/src/core/dom/attributes.js +33 -0
- package/src/core/dom/classes.js +70 -0
- package/src/core/dom/create.js +133 -0
- package/src/core/dom/events.js +175 -0
- package/src/core/dom/index.js +5 -0
- package/src/core/dom/utils.js +22 -0
- package/src/core/index.js +23 -0
- package/src/core/layout/index.js +93 -0
- package/src/core/state/disabled.js +14 -0
- package/src/core/state/emitter.js +63 -0
- package/src/core/state/events.js +29 -0
- package/src/core/state/index.js +6 -0
- package/src/core/state/lifecycle.js +64 -0
- package/src/core/state/store.js +112 -0
- package/src/core/utils/index.js +39 -0
- package/src/core/utils/mobile.js +74 -0
- package/src/core/utils/object.js +22 -0
- package/src/core/utils/validate.js +37 -0
- package/src/index.js +11 -0
- package/src/styles/abstract/_base.scss +2 -0
- package/src/styles/abstract/_config.scss +28 -0
- package/src/styles/abstract/_functions.scss +124 -0
- package/src/styles/abstract/_mixins.scss +261 -0
- package/src/styles/abstract/_variables.scss +158 -0
- package/src/styles/main.scss +78 -0
- package/src/styles/themes/_base-theme.scss +49 -0
- package/src/styles/themes/_baseline.scss +90 -0
- package/src/styles/themes/_forest.scss +71 -0
- package/src/styles/themes/_index.scss +6 -0
- package/src/styles/themes/_ocean.scss +71 -0
- package/src/styles/themes/_sunset.scss +55 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// src/core/collection/list-manager.js
|
|
2
|
+
|
|
3
|
+
import { createRouteAdapter } from './adapters/route'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a list manager for a specific collection
|
|
7
|
+
* @param {string} collection - Collection name
|
|
8
|
+
* @param {Object} config - Configuration options
|
|
9
|
+
* @param {Function} config.transform - Transform function for items
|
|
10
|
+
* @param {string} config.baseUrl - Base API URL
|
|
11
|
+
* @returns {Object} List manager methods
|
|
12
|
+
*/
|
|
13
|
+
export const createListManager = (collection, config = {}) => {
|
|
14
|
+
const {
|
|
15
|
+
transform = (item) => item,
|
|
16
|
+
baseUrl = 'http://localhost:4000/api'
|
|
17
|
+
} = config
|
|
18
|
+
|
|
19
|
+
// Initialize route adapter
|
|
20
|
+
const adapter = createRouteAdapter({
|
|
21
|
+
base: baseUrl,
|
|
22
|
+
endpoints: {
|
|
23
|
+
list: `/${collection}`
|
|
24
|
+
},
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json'
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Load items with cursor pagination
|
|
31
|
+
const loadItems = async (params = {}) => {
|
|
32
|
+
try {
|
|
33
|
+
const response = await adapter.read(params)
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
items: response.items.map(transform),
|
|
37
|
+
meta: response.meta
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`Error loading ${collection}:`, error)
|
|
41
|
+
return {
|
|
42
|
+
items: [],
|
|
43
|
+
meta: {
|
|
44
|
+
cursor: null,
|
|
45
|
+
hasNext: false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Utility to create a cursor-based page loader
|
|
52
|
+
const createPageLoader = (list, { onLoad, pageSize = 20 } = {}) => {
|
|
53
|
+
let currentCursor = null
|
|
54
|
+
let loading = false
|
|
55
|
+
const pageHistory = []
|
|
56
|
+
|
|
57
|
+
const load = async (cursor = null, addToHistory = true) => {
|
|
58
|
+
if (loading) return
|
|
59
|
+
|
|
60
|
+
loading = true
|
|
61
|
+
onLoad?.({ loading: true })
|
|
62
|
+
|
|
63
|
+
const { items, meta } = await loadItems({
|
|
64
|
+
limit: pageSize,
|
|
65
|
+
cursor
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (addToHistory && cursor) {
|
|
69
|
+
pageHistory.push(currentCursor)
|
|
70
|
+
}
|
|
71
|
+
currentCursor = meta.cursor
|
|
72
|
+
|
|
73
|
+
list.setItems(items)
|
|
74
|
+
loading = false
|
|
75
|
+
|
|
76
|
+
onLoad?.({
|
|
77
|
+
loading: false,
|
|
78
|
+
hasNext: meta.hasNext,
|
|
79
|
+
hasPrev: pageHistory.length > 0,
|
|
80
|
+
items
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
hasNext: meta.hasNext,
|
|
85
|
+
hasPrev: pageHistory.length > 0
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const loadNext = () => load(currentCursor)
|
|
90
|
+
|
|
91
|
+
const loadPrev = () => {
|
|
92
|
+
const previousCursor = pageHistory.pop()
|
|
93
|
+
return load(previousCursor, false)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
load,
|
|
98
|
+
loadNext,
|
|
99
|
+
loadPrev,
|
|
100
|
+
get loading () { return loading },
|
|
101
|
+
get cursor () { return currentCursor }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
loadItems,
|
|
107
|
+
createPageLoader
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Transform functions for common collections
|
|
113
|
+
*/
|
|
114
|
+
export const transforms = {
|
|
115
|
+
track: (track) => ({
|
|
116
|
+
id: track._id,
|
|
117
|
+
headline: track.title || 'Untitled',
|
|
118
|
+
supportingText: track.artist || 'Unknown Artist',
|
|
119
|
+
meta: track.year?.toString() || ''
|
|
120
|
+
}),
|
|
121
|
+
|
|
122
|
+
playlist: (playlist) => ({
|
|
123
|
+
id: playlist._id,
|
|
124
|
+
headline: playlist.name || 'Untitled Playlist',
|
|
125
|
+
supportingText: `${playlist.tracks?.length || 0} tracks`,
|
|
126
|
+
meta: playlist.creator || ''
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
country: (country) => ({
|
|
130
|
+
id: country._id,
|
|
131
|
+
headline: country.name || country.code,
|
|
132
|
+
supportingText: country.continent || '',
|
|
133
|
+
meta: country.code || ''
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Usage example:
|
|
139
|
+
*
|
|
140
|
+
* const trackManager = createListManager('track', {
|
|
141
|
+
* transform: transforms.track
|
|
142
|
+
* })
|
|
143
|
+
*
|
|
144
|
+
* const loader = trackManager.createPageLoader(list, {
|
|
145
|
+
* onLoad: ({ loading, hasNext, items }) => {
|
|
146
|
+
* updateNavigation({ loading, hasNext })
|
|
147
|
+
* logEvent(`Loaded ${items.length} tracks`)
|
|
148
|
+
* }
|
|
149
|
+
* })
|
|
150
|
+
*
|
|
151
|
+
* // Initial load
|
|
152
|
+
* await loader.load()
|
|
153
|
+
*
|
|
154
|
+
* // Navigation
|
|
155
|
+
* nextButton.onclick = () => loader.loadNext()
|
|
156
|
+
* prevButton.onclick = () => loader.loadPrev()
|
|
157
|
+
*/
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/core/compose/component.js
|
|
2
|
+
/**
|
|
3
|
+
* @module core/compose/component
|
|
4
|
+
* @description Core utilities for component composition and creation with built-in mobile support
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createElement } from '../dom/create'
|
|
8
|
+
import {
|
|
9
|
+
normalizeEvent,
|
|
10
|
+
hasTouchSupport,
|
|
11
|
+
TOUCH_CONFIG,
|
|
12
|
+
PASSIVE_EVENTS
|
|
13
|
+
} from '../utils/mobile'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates helper functions for managing CSS class names with a prefix
|
|
17
|
+
* @param {string} prefix - Prefix to apply to class names
|
|
18
|
+
* @returns {Object} Class name utilities
|
|
19
|
+
* @property {Function} getClass - Gets a class name with prefix
|
|
20
|
+
* @property {Function} getModifierClass - Gets a modifier class with prefix
|
|
21
|
+
* @property {Function} getElementClass - Gets an element class with prefix
|
|
22
|
+
* @example
|
|
23
|
+
* const { getClass } = withPrefix('mtrl');
|
|
24
|
+
* getClass('button'); // Returns 'mtrl-button'
|
|
25
|
+
*/
|
|
26
|
+
const withPrefix = prefix => ({
|
|
27
|
+
/**
|
|
28
|
+
* Gets a prefixed class name
|
|
29
|
+
* @param {string} name - Base class name
|
|
30
|
+
* @returns {string} Prefixed class name
|
|
31
|
+
*/
|
|
32
|
+
getClass: (name) => `${prefix}-${name}`,
|
|
33
|
+
/**
|
|
34
|
+
* Gets a prefixed modifier class name
|
|
35
|
+
* @param {string} base - Base class name
|
|
36
|
+
* @param {string} modifier - Modifier name
|
|
37
|
+
* @returns {string} Prefixed modifier class
|
|
38
|
+
*/
|
|
39
|
+
getModifierClass: (base, modifier) => `${base}--${modifier}`,
|
|
40
|
+
/**
|
|
41
|
+
* Gets a prefixed element class name
|
|
42
|
+
* @param {string} base - Base class name
|
|
43
|
+
* @param {string} element - Element name
|
|
44
|
+
* @returns {string} Prefixed element class
|
|
45
|
+
*/
|
|
46
|
+
getElementClass: (base, element) => `${base}-${element}`
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a base component with configuration and prefix utilities.
|
|
51
|
+
* This forms the foundation for all components in the system.
|
|
52
|
+
*
|
|
53
|
+
* @param {Object} config - Component configuration
|
|
54
|
+
* @param {string} [config.prefix='mtrl'] - CSS class prefix
|
|
55
|
+
* @param {string} [config.componentName] - Component name for class generation
|
|
56
|
+
* @returns {Object} Base component with prefix utilities
|
|
57
|
+
*/
|
|
58
|
+
export const createBase = (config = {}) => ({
|
|
59
|
+
config,
|
|
60
|
+
componentName: config.componentName,
|
|
61
|
+
...withPrefix(config.prefix || 'mtrl'),
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Manages the touch interaction state for the component.
|
|
65
|
+
* This helps track touch gestures and interactions.
|
|
66
|
+
*/
|
|
67
|
+
touchState: {
|
|
68
|
+
startTime: 0,
|
|
69
|
+
startPosition: { x: 0, y: 0 },
|
|
70
|
+
isTouching: false,
|
|
71
|
+
activeTarget: null
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Updates the component's touch state based on user interactions.
|
|
76
|
+
* Tracks touch position and timing for gesture recognition.
|
|
77
|
+
*/
|
|
78
|
+
updateTouchState (event, status) {
|
|
79
|
+
const normalized = normalizeEvent(event)
|
|
80
|
+
|
|
81
|
+
if (status === 'start') {
|
|
82
|
+
this.touchState = {
|
|
83
|
+
startTime: Date.now(),
|
|
84
|
+
startPosition: {
|
|
85
|
+
x: normalized.clientX,
|
|
86
|
+
y: normalized.clientY
|
|
87
|
+
},
|
|
88
|
+
isTouching: true,
|
|
89
|
+
activeTarget: normalized.target
|
|
90
|
+
}
|
|
91
|
+
} else if (status === 'end') {
|
|
92
|
+
this.touchState.isTouching = false
|
|
93
|
+
this.touchState.activeTarget = null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Higher-order function that adds a DOM element to a component
|
|
100
|
+
* @param {Object} options - Element creation options
|
|
101
|
+
* @param {string} [options.tag='div'] - HTML tag name
|
|
102
|
+
* @param {string} [options.componentName] - Component name for class generation
|
|
103
|
+
* @param {Object} [options.attrs] - HTML attributes
|
|
104
|
+
* @param {string|string[]} [options.className] - Additional CSS classes
|
|
105
|
+
* @param {Object} [options.forwardEvents] - Native events to forward to component events
|
|
106
|
+
* @returns {Function} Component enhancer
|
|
107
|
+
* @example
|
|
108
|
+
* pipe(
|
|
109
|
+
* createBase,
|
|
110
|
+
* withElement({
|
|
111
|
+
* tag: 'button',
|
|
112
|
+
* componentName: 'button',
|
|
113
|
+
* attrs: { type: 'button' },
|
|
114
|
+
* forwardEvents: {
|
|
115
|
+
* click: component => !component.element.disabled
|
|
116
|
+
* }
|
|
117
|
+
* })
|
|
118
|
+
* )({ prefix: 'app' })
|
|
119
|
+
*/
|
|
120
|
+
export const withElement = (options = {}) => (base) => {
|
|
121
|
+
/**
|
|
122
|
+
* Handles the start of a touch interaction.
|
|
123
|
+
* Initializes touch tracking and provides visual feedback.
|
|
124
|
+
*/
|
|
125
|
+
const handleTouchStart = (event) => {
|
|
126
|
+
base.updateTouchState(event, 'start')
|
|
127
|
+
element.classList.add(`${base.getClass('touch-active')}`)
|
|
128
|
+
|
|
129
|
+
if (options.forwardEvents?.touchstart) {
|
|
130
|
+
base.emit?.('touchstart', normalizeEvent(event))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Handles the end of a touch interaction.
|
|
136
|
+
* Detects taps and cleans up touch state.
|
|
137
|
+
*/
|
|
138
|
+
const handleTouchEnd = (event) => {
|
|
139
|
+
if (!base.touchState.isTouching) return
|
|
140
|
+
|
|
141
|
+
const touchDuration = Date.now() - base.touchState.startTime
|
|
142
|
+
element.classList.remove(`${base.getClass('touch-active')}`)
|
|
143
|
+
base.updateTouchState(event, 'end')
|
|
144
|
+
|
|
145
|
+
// Emit tap event for short touches
|
|
146
|
+
if (touchDuration < TOUCH_CONFIG.TAP_THRESHOLD) {
|
|
147
|
+
base.emit?.('tap', normalizeEvent(event))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (options.forwardEvents?.touchend) {
|
|
151
|
+
base.emit?.('touchend', normalizeEvent(event))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handles touch movement.
|
|
157
|
+
* Detects swipes and other gesture-based interactions.
|
|
158
|
+
*/
|
|
159
|
+
const handleTouchMove = (event) => {
|
|
160
|
+
if (!base.touchState.isTouching) return
|
|
161
|
+
|
|
162
|
+
const normalized = normalizeEvent(event)
|
|
163
|
+
const deltaX = normalized.clientX - base.touchState.startPosition.x
|
|
164
|
+
const deltaY = normalized.clientY - base.touchState.startPosition.y
|
|
165
|
+
|
|
166
|
+
// Detect and emit swipe gestures
|
|
167
|
+
if (Math.abs(deltaX) > TOUCH_CONFIG.SWIPE_THRESHOLD) {
|
|
168
|
+
base.emit?.('swipe', {
|
|
169
|
+
direction: deltaX > 0 ? 'right' : 'left',
|
|
170
|
+
deltaX,
|
|
171
|
+
deltaY
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (options.forwardEvents?.touchmove) {
|
|
176
|
+
base.emit?.('touchmove', { ...normalized, deltaX, deltaY })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Create the element with appropriate classes
|
|
181
|
+
const element = createElement({
|
|
182
|
+
...options,
|
|
183
|
+
className: [
|
|
184
|
+
base.getClass(options.componentName || base.componentName || 'component'),
|
|
185
|
+
hasTouchSupport() && options.interactive ? base.getClass('interactive') : null,
|
|
186
|
+
options.className
|
|
187
|
+
].filter(Boolean),
|
|
188
|
+
context: base
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Add event listeners only if touch is supported and the component is interactive
|
|
192
|
+
if (hasTouchSupport() && options.interactive) {
|
|
193
|
+
element.addEventListener('touchstart', handleTouchStart, PASSIVE_EVENTS)
|
|
194
|
+
element.addEventListener('touchend', handleTouchEnd)
|
|
195
|
+
element.addEventListener('touchmove', handleTouchMove, PASSIVE_EVENTS)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...base,
|
|
200
|
+
element,
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Adds CSS classes to the element
|
|
204
|
+
* @param {...string} classes - CSS classes to add
|
|
205
|
+
* @returns {Object} Component instance for chaining
|
|
206
|
+
*/
|
|
207
|
+
addClass (...classes) {
|
|
208
|
+
element.classList.add(...classes.filter(Boolean))
|
|
209
|
+
return this
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Removes the element and cleans up event listeners.
|
|
214
|
+
* Ensures proper resource cleanup when the component is destroyed.
|
|
215
|
+
*/
|
|
216
|
+
destroy () {
|
|
217
|
+
if (hasTouchSupport() && options.interactive) {
|
|
218
|
+
element.removeEventListener('touchstart', handleTouchStart)
|
|
219
|
+
element.removeEventListener('touchend', handleTouchEnd)
|
|
220
|
+
element.removeEventListener('touchmove', handleTouchMove)
|
|
221
|
+
}
|
|
222
|
+
element.remove()
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/core/compose/features/checkable.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adds checked state management to a component with an input
|
|
5
|
+
* Manages visual state and event emission for checked changes
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} config - Checkable configuration
|
|
8
|
+
* @param {boolean} [config.checked] - Initial checked state
|
|
9
|
+
*
|
|
10
|
+
* @returns {Function} Component transformer that adds checkable functionality
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const component = pipe(
|
|
14
|
+
* createBase,
|
|
15
|
+
* withEvents(),
|
|
16
|
+
* withInput(config),
|
|
17
|
+
* withCheckable({ checked: true })
|
|
18
|
+
* )(config);
|
|
19
|
+
*
|
|
20
|
+
* // Use the checkable API
|
|
21
|
+
* component.checkable.toggle();
|
|
22
|
+
* component.checkable.check();
|
|
23
|
+
* component.checkable.uncheck();
|
|
24
|
+
*
|
|
25
|
+
* // Listen for changes
|
|
26
|
+
* component.on('change', ({ checked }) => {
|
|
27
|
+
* console.log('State changed:', checked);
|
|
28
|
+
* });
|
|
29
|
+
*/
|
|
30
|
+
export const withCheckable = (config = {}) => (component) => {
|
|
31
|
+
if (!component.input) return component
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Updates component classes to reflect checked state
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
const updateStateClasses = () => {
|
|
38
|
+
component.element.classList.toggle(
|
|
39
|
+
`${component.getClass('switch')}--checked`,
|
|
40
|
+
component.input.checked
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Set initial state
|
|
45
|
+
if (config.checked) {
|
|
46
|
+
component.input.checked = true
|
|
47
|
+
updateStateClasses()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Update classes whenever checked state changes
|
|
51
|
+
component.on('change', updateStateClasses)
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...component,
|
|
55
|
+
checkable: {
|
|
56
|
+
/**
|
|
57
|
+
* Sets the checked state to true
|
|
58
|
+
* Emits change event if state changes
|
|
59
|
+
* @returns {Object} Checkable interface
|
|
60
|
+
*/
|
|
61
|
+
check () {
|
|
62
|
+
if (!component.input.checked) {
|
|
63
|
+
component.input.checked = true
|
|
64
|
+
updateStateClasses()
|
|
65
|
+
component.emit('change', {
|
|
66
|
+
checked: true,
|
|
67
|
+
value: component.input.value
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
return this
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sets the checked state to false
|
|
75
|
+
* Emits change event if state changes
|
|
76
|
+
* @returns {Object} Checkable interface
|
|
77
|
+
*/
|
|
78
|
+
uncheck () {
|
|
79
|
+
if (component.input.checked) {
|
|
80
|
+
component.input.checked = false
|
|
81
|
+
updateStateClasses()
|
|
82
|
+
component.emit('change', {
|
|
83
|
+
checked: false,
|
|
84
|
+
value: component.input.value
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
return this
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Toggles the current checked state
|
|
92
|
+
* Always emits change event
|
|
93
|
+
* @returns {Object} Checkable interface
|
|
94
|
+
*/
|
|
95
|
+
toggle () {
|
|
96
|
+
component.input.checked = !component.input.checked
|
|
97
|
+
updateStateClasses()
|
|
98
|
+
component.emit('change', {
|
|
99
|
+
checked: component.input.checked,
|
|
100
|
+
value: component.input.value
|
|
101
|
+
})
|
|
102
|
+
return this
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Gets the current checked state
|
|
107
|
+
* @returns {boolean} Whether component is checked
|
|
108
|
+
*/
|
|
109
|
+
isChecked () {
|
|
110
|
+
return component.input.checked
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/core/compose/features/disabled.js
|
|
2
|
+
|
|
3
|
+
export const withDisabled = (config) => (component) => {
|
|
4
|
+
if (!component.element) return component
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
...component,
|
|
8
|
+
disabled: {
|
|
9
|
+
enable () {
|
|
10
|
+
console.debug('disabled')
|
|
11
|
+
component.element.disabled = false
|
|
12
|
+
const className = `${config.prefix}-${config.componentName}--disable`
|
|
13
|
+
component.element.classList.remove(className)
|
|
14
|
+
return this
|
|
15
|
+
},
|
|
16
|
+
disable () {
|
|
17
|
+
console.debug('disabled')
|
|
18
|
+
component.element.disabled = true
|
|
19
|
+
const className = `${config.prefix}-${config.componentName}--disable`
|
|
20
|
+
component.element.classList.add(className)
|
|
21
|
+
return this
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/core/compose/features/withEvents.js
|
|
2
|
+
/**
|
|
3
|
+
* @module core/compose/features
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createEmitter } from '../../state/emitter'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Adds event handling capabilities to a component
|
|
10
|
+
* @memberof module:core/compose/features
|
|
11
|
+
* @function withEvents
|
|
12
|
+
* @param {HTMLElement} [target] - Event target element
|
|
13
|
+
* @returns {Function} Component transformer
|
|
14
|
+
* @example
|
|
15
|
+
* const button = pipe(
|
|
16
|
+
* createBase({ componentName: 'button' }),
|
|
17
|
+
* withElement(),
|
|
18
|
+
* withEvents()
|
|
19
|
+
* )({})
|
|
20
|
+
*
|
|
21
|
+
* button.on('click', () => console.log('clicked'))
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adds event handling capabilities to a component
|
|
26
|
+
* Returns event system ready to use immediately
|
|
27
|
+
*/
|
|
28
|
+
export const withEvents = () => (component) => {
|
|
29
|
+
const emitter = createEmitter()
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...component,
|
|
33
|
+
on (event, handler) {
|
|
34
|
+
emitter.on(event, handler)
|
|
35
|
+
return this
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
off (event, handler) {
|
|
39
|
+
emitter.off(event, handler)
|
|
40
|
+
return this
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
emit (event, data) {
|
|
44
|
+
emitter.emit(event, data)
|
|
45
|
+
return this
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/core/compose/features/icon.js
|
|
2
|
+
import { createIcon } from '../../../core/build/icon'
|
|
3
|
+
|
|
4
|
+
const updateCircularStyle = (component, config) => {
|
|
5
|
+
const hasText = config.text
|
|
6
|
+
const hasIcon = config.icon
|
|
7
|
+
|
|
8
|
+
const circularClass = `${component.getClass('button')}--circular`
|
|
9
|
+
if (!hasText && hasIcon) {
|
|
10
|
+
component.element.classList.add(circularClass)
|
|
11
|
+
} else {
|
|
12
|
+
component.element.classList.remove(circularClass)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const withIcon = (config = {}) => (component) => {
|
|
17
|
+
const icon = createIcon(component.element, {
|
|
18
|
+
prefix: config.prefix,
|
|
19
|
+
type: 'button',
|
|
20
|
+
position: config.iconPosition
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
if (config.icon) {
|
|
24
|
+
icon.setIcon(config.icon)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
updateCircularStyle(component, config)
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
...component,
|
|
31
|
+
icon
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/core/compose/features/index.js
|
|
2
|
+
|
|
3
|
+
// Core features
|
|
4
|
+
export { withEvents } from './events'
|
|
5
|
+
export { withText } from './text'
|
|
6
|
+
export { withIcon } from './icon'
|
|
7
|
+
export { withVariant } from './variant'
|
|
8
|
+
export { withSize } from './size'
|
|
9
|
+
export { withPosition } from './position'
|
|
10
|
+
export { withInput } from './input'
|
|
11
|
+
export { withTrack } from './track'
|
|
12
|
+
export { withTextInput } from './textinput'
|
|
13
|
+
export { withTextLabel } from './textlabel'
|
|
14
|
+
export { withRipple } from './ripple'
|
|
15
|
+
|
|
16
|
+
// State management features
|
|
17
|
+
export { withDisabled } from './disabled'
|
|
18
|
+
export { withCheckable } from './checkable'
|
|
19
|
+
|
|
20
|
+
export { withLifecycle } from './lifecycle'
|