mtrl 0.2.5 → 0.2.7
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/index.ts +18 -0
- package/package.json +1 -1
- package/src/components/badge/_styles.scss +123 -115
- package/src/components/badge/api.ts +57 -59
- package/src/components/badge/badge.ts +16 -2
- package/src/components/badge/config.ts +65 -11
- package/src/components/badge/constants.ts +22 -12
- package/src/components/badge/features.ts +44 -40
- package/src/components/badge/types.ts +42 -30
- package/src/components/bottom-app-bar/_styles.scss +103 -0
- package/src/components/bottom-app-bar/bottom-app-bar.ts +196 -0
- package/src/components/bottom-app-bar/config.ts +73 -0
- package/src/components/bottom-app-bar/index.ts +11 -0
- package/src/components/bottom-app-bar/types.ts +108 -0
- package/src/components/button/_styles.scss +0 -66
- package/src/components/button/api.ts +5 -0
- package/src/components/button/button.ts +0 -2
- package/src/components/button/config.ts +5 -0
- package/src/components/button/constants.ts +0 -6
- package/src/components/button/index.ts +2 -2
- package/src/components/button/types.ts +7 -7
- package/src/components/card/_styles.scss +67 -25
- package/src/components/card/api.ts +54 -3
- package/src/components/card/card.ts +25 -6
- package/src/components/card/config.ts +189 -22
- package/src/components/card/constants.ts +20 -19
- package/src/components/card/content.ts +299 -2
- package/src/components/card/features.ts +158 -4
- package/src/components/card/index.ts +31 -9
- package/src/components/card/types.ts +166 -15
- package/src/components/checkbox/_styles.scss +0 -2
- package/src/components/chip/chip.ts +1 -9
- package/src/components/chip/constants.ts +0 -10
- package/src/components/chip/index.ts +1 -1
- package/src/components/chip/types.ts +1 -4
- package/src/components/datepicker/_styles.scss +358 -0
- package/src/components/datepicker/api.ts +272 -0
- package/src/components/datepicker/config.ts +144 -0
- package/src/components/datepicker/constants.ts +98 -0
- package/src/components/datepicker/datepicker.ts +346 -0
- package/src/components/datepicker/index.ts +9 -0
- package/src/components/datepicker/render.ts +452 -0
- package/src/components/datepicker/types.ts +268 -0
- package/src/components/datepicker/utils.ts +290 -0
- package/src/components/dialog/_styles.scss +174 -128
- package/src/components/dialog/api.ts +48 -13
- package/src/components/dialog/config.ts +9 -5
- package/src/components/dialog/dialog.ts +6 -3
- package/src/components/dialog/features.ts +290 -130
- package/src/components/dialog/types.ts +7 -4
- package/src/components/divider/_styles.scss +57 -0
- package/src/components/divider/config.ts +81 -0
- package/src/components/divider/divider.ts +37 -0
- package/src/components/divider/features.ts +207 -0
- package/src/components/divider/index.ts +5 -0
- package/src/components/divider/types.ts +55 -0
- package/src/components/extended-fab/_styles.scss +267 -0
- package/src/components/extended-fab/api.ts +141 -0
- package/src/components/extended-fab/config.ts +108 -0
- package/src/components/extended-fab/constants.ts +36 -0
- package/src/components/extended-fab/extended-fab.ts +125 -0
- package/src/components/extended-fab/index.ts +4 -0
- package/src/components/extended-fab/types.ts +287 -0
- package/src/components/fab/_styles.scss +225 -0
- package/src/components/fab/api.ts +97 -0
- package/src/components/fab/config.ts +94 -0
- package/src/components/fab/constants.ts +41 -0
- package/src/components/fab/fab.ts +67 -0
- package/src/components/fab/index.ts +4 -0
- package/src/components/fab/types.ts +234 -0
- package/src/components/navigation/_styles.scss +1 -0
- package/src/components/navigation/api.ts +78 -50
- package/src/components/navigation/features/items.ts +280 -0
- package/src/components/navigation/nav-item.ts +72 -23
- package/src/components/navigation/navigation.ts +54 -2
- package/src/components/navigation/types.ts +210 -188
- package/src/components/progress/_styles.scss +0 -65
- package/src/components/progress/config.ts +1 -2
- package/src/components/progress/constants.ts +0 -14
- package/src/components/progress/index.ts +1 -1
- package/src/components/progress/progress.ts +1 -4
- package/src/components/progress/types.ts +1 -4
- package/src/components/radios/_styles.scss +0 -45
- package/src/components/radios/api.ts +85 -60
- package/src/components/radios/config.ts +1 -2
- package/src/components/radios/constants.ts +0 -9
- package/src/components/radios/index.ts +1 -1
- package/src/components/radios/radio.ts +34 -11
- package/src/components/radios/radios.ts +2 -1
- package/src/components/radios/types.ts +1 -7
- package/src/components/search/_styles.scss +306 -0
- package/src/components/search/api.ts +203 -0
- package/src/components/search/config.ts +87 -0
- package/src/components/search/constants.ts +21 -0
- package/src/components/search/features/index.ts +4 -0
- package/src/components/search/features/search.ts +718 -0
- package/src/components/search/features/states.ts +165 -0
- package/src/components/search/features/structure.ts +198 -0
- package/src/components/search/index.ts +10 -0
- package/src/components/search/search.ts +52 -0
- package/src/components/search/types.ts +163 -0
- package/src/components/segmented-button/_styles.scss +117 -0
- package/src/components/segmented-button/config.ts +67 -0
- package/src/components/segmented-button/constants.ts +42 -0
- package/src/components/segmented-button/index.ts +4 -0
- package/src/components/segmented-button/segment.ts +155 -0
- package/src/components/segmented-button/segmented-button.ts +250 -0
- package/src/components/segmented-button/types.ts +219 -0
- package/src/components/slider/_styles.scss +221 -168
- package/src/components/slider/accessibility.md +59 -0
- package/src/components/slider/api.ts +41 -120
- package/src/components/slider/config.ts +51 -49
- package/src/components/slider/features/handlers.ts +495 -0
- package/src/components/slider/features/index.ts +1 -2
- package/src/components/slider/features/slider.ts +66 -84
- package/src/components/slider/features/states.ts +195 -0
- package/src/components/slider/features/structure.ts +141 -184
- package/src/components/slider/features/ui.ts +150 -201
- package/src/components/slider/index.ts +2 -11
- package/src/components/slider/slider.ts +9 -12
- package/src/components/slider/types.ts +39 -24
- package/src/components/switch/_styles.scss +0 -2
- package/src/components/tabs/_styles.scss +346 -154
- package/src/components/tabs/api.ts +178 -400
- package/src/components/tabs/config.ts +46 -52
- package/src/components/tabs/constants.ts +85 -8
- package/src/components/tabs/features.ts +403 -0
- package/src/components/tabs/index.ts +60 -3
- package/src/components/tabs/indicator.ts +285 -0
- package/src/components/tabs/responsive.ts +144 -0
- package/src/components/tabs/scroll-indicators.ts +149 -0
- package/src/components/tabs/state.ts +186 -0
- package/src/components/tabs/tab-api.ts +258 -0
- package/src/components/tabs/tab.ts +255 -0
- package/src/components/tabs/tabs.ts +50 -31
- package/src/components/tabs/types.ts +332 -128
- package/src/components/tabs/utils.ts +107 -0
- package/src/components/textfield/_styles.scss +0 -98
- package/src/components/textfield/config.ts +2 -3
- package/src/components/textfield/constants.ts +0 -14
- package/src/components/textfield/index.ts +2 -2
- package/src/components/textfield/textfield.ts +0 -2
- package/src/components/textfield/types.ts +1 -4
- package/src/components/timepicker/README.md +277 -0
- package/src/components/timepicker/_styles.scss +451 -0
- package/src/components/timepicker/api.ts +632 -0
- package/src/components/timepicker/clockdial.ts +482 -0
- package/src/components/timepicker/config.ts +130 -0
- package/src/components/timepicker/constants.ts +138 -0
- package/src/components/timepicker/index.ts +8 -0
- package/src/components/timepicker/render.ts +613 -0
- package/src/components/timepicker/timepicker.ts +117 -0
- package/src/components/timepicker/types.ts +336 -0
- package/src/components/timepicker/utils.ts +241 -0
- package/src/components/top-app-bar/_styles.scss +225 -0
- package/src/components/top-app-bar/config.ts +83 -0
- package/src/components/top-app-bar/index.ts +11 -0
- package/src/components/top-app-bar/top-app-bar.ts +316 -0
- package/src/components/top-app-bar/types.ts +140 -0
- package/src/core/build/_ripple.scss +6 -6
- package/src/core/build/ripple.ts +72 -95
- package/src/core/compose/component.ts +1 -1
- package/src/core/compose/features/badge.ts +79 -0
- package/src/core/compose/features/icon.ts +3 -1
- package/src/core/compose/features/index.ts +3 -1
- package/src/core/compose/features/ripple.ts +4 -1
- package/src/core/compose/features/textlabel.ts +26 -2
- package/src/core/dom/create.ts +5 -0
- package/src/index.ts +9 -0
- package/src/styles/abstract/_theme.scss +115 -3
- package/src/styles/themes/_autumn.scss +21 -0
- package/src/styles/themes/_base-theme.scss +61 -0
- package/src/styles/themes/_baseline.scss +58 -0
- package/src/styles/themes/_bluekhaki.scss +125 -0
- package/src/styles/themes/_brownbeige.scss +125 -0
- package/src/styles/themes/_browngreen.scss +125 -0
- package/src/styles/themes/_forest.scss +6 -0
- package/src/styles/themes/_greenbeige.scss +125 -0
- package/src/styles/themes/_material.scss +125 -0
- package/src/styles/themes/_ocean.scss +6 -0
- package/src/styles/themes/_sageivory.scss +125 -0
- package/src/styles/themes/_spring.scss +6 -0
- package/src/styles/themes/_summer.scss +5 -0
- package/src/styles/themes/_sunset.scss +5 -0
- package/src/styles/themes/_tealcaramel.scss +125 -0
- package/src/styles/themes/_winter.scss +6 -0
- package/src/components/card/actions.ts +0 -48
- package/src/components/card/header.ts +0 -88
- package/src/components/card/media.ts +0 -52
- package/src/components/navigation/features/items.js +0 -192
- package/src/components/slider/features/appearance.ts +0 -94
- package/src/components/slider/features/disabled.ts +0 -43
- package/src/components/slider/features/events.ts +0 -164
- package/src/components/slider/features/interactions.ts +0 -261
- package/src/components/slider/features/keyboard.ts +0 -112
- package/src/core/collection/adapters/mongodb.js +0 -232
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
// src/components/search/features/search.ts
|
|
2
|
+
import { SEARCH_EVENTS, SEARCH_VARIANTS } from '../constants';
|
|
3
|
+
import { SearchConfig } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Add main search functionality to component
|
|
7
|
+
* @param config Search configuration
|
|
8
|
+
* @returns Component enhancer with search functionality
|
|
9
|
+
*/
|
|
10
|
+
export const withSearch = (config: SearchConfig) => component => {
|
|
11
|
+
// Ensure component has events capability
|
|
12
|
+
if (!component.emit) {
|
|
13
|
+
console.warn('Search component requires event emission capability');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Ensure component structure exists
|
|
17
|
+
if (!component.structure) {
|
|
18
|
+
console.error('Search component missing structure');
|
|
19
|
+
return component;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Get elements from structure
|
|
23
|
+
const {
|
|
24
|
+
container,
|
|
25
|
+
input,
|
|
26
|
+
inputWrapper,
|
|
27
|
+
leadingIcon,
|
|
28
|
+
clearButton,
|
|
29
|
+
trailingIcon,
|
|
30
|
+
trailingIcon2,
|
|
31
|
+
avatar,
|
|
32
|
+
divider,
|
|
33
|
+
suggestionsContainer
|
|
34
|
+
} = component.structure;
|
|
35
|
+
|
|
36
|
+
// Create state object
|
|
37
|
+
const state = {
|
|
38
|
+
value: config.value || '',
|
|
39
|
+
placeholder: config.placeholder || 'Search',
|
|
40
|
+
suggestions: config.suggestions || [],
|
|
41
|
+
isFocused: false,
|
|
42
|
+
isExpanded: config.variant === SEARCH_VARIANTS.VIEW,
|
|
43
|
+
component
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Create event helpers
|
|
47
|
+
const eventHelpers = {
|
|
48
|
+
triggerEvent(eventName, originalEvent = null) {
|
|
49
|
+
const eventData = {
|
|
50
|
+
search: state.component,
|
|
51
|
+
value: state.value,
|
|
52
|
+
originalEvent,
|
|
53
|
+
preventDefault: () => { eventData.defaultPrevented = true; },
|
|
54
|
+
defaultPrevented: false
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (component.emit) {
|
|
58
|
+
component.emit(eventName, eventData);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Call onEvent handlers from config if they exist
|
|
62
|
+
const handlerName = `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`;
|
|
63
|
+
if (config[handlerName] && typeof config[handlerName] === 'function') {
|
|
64
|
+
config[handlerName](state.value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return eventData;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Updates the value and UI
|
|
73
|
+
*/
|
|
74
|
+
const updateValue = (newValue, triggerEvent = true) => {
|
|
75
|
+
// Update internal state
|
|
76
|
+
state.value = newValue;
|
|
77
|
+
|
|
78
|
+
// Update input value
|
|
79
|
+
if (input) {
|
|
80
|
+
input.value = newValue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Show/hide clear button
|
|
84
|
+
if (clearButton) {
|
|
85
|
+
if (newValue) {
|
|
86
|
+
clearButton.classList.remove(`${component.getClass('search-clear-button')}--hidden`);
|
|
87
|
+
if (!component.disabled?.isDisabled()) {
|
|
88
|
+
clearButton.tabIndex = 0;
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
clearButton.classList.add(`${component.getClass('search-clear-button')}--hidden`);
|
|
92
|
+
clearButton.tabIndex = -1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Trigger input event
|
|
97
|
+
if (triggerEvent) {
|
|
98
|
+
eventHelpers.triggerEvent(SEARCH_EVENTS.INPUT);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Submits the current search value
|
|
104
|
+
*/
|
|
105
|
+
const submitSearch = () => {
|
|
106
|
+
if (state.value) {
|
|
107
|
+
eventHelpers.triggerEvent(SEARCH_EVENTS.SUBMIT);
|
|
108
|
+
|
|
109
|
+
// Hide suggestions if in bar mode
|
|
110
|
+
if (!state.isExpanded) {
|
|
111
|
+
hideSuggestions();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Clears the search input
|
|
118
|
+
*/
|
|
119
|
+
const clearSearch = (triggerEvent = true) => {
|
|
120
|
+
updateValue('', triggerEvent);
|
|
121
|
+
|
|
122
|
+
if (input) {
|
|
123
|
+
input.focus();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (triggerEvent) {
|
|
127
|
+
eventHelpers.triggerEvent(SEARCH_EVENTS.CLEAR);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Shows suggestions
|
|
133
|
+
*/
|
|
134
|
+
const showSuggestions = () => {
|
|
135
|
+
if (suggestionsContainer) {
|
|
136
|
+
renderSuggestions();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Hides suggestions
|
|
142
|
+
*/
|
|
143
|
+
const hideSuggestions = () => {
|
|
144
|
+
if (suggestionsContainer) {
|
|
145
|
+
suggestionsContainer.classList.remove(`${component.getClass('search-suggestions-container')}--visible`);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Expands search bar to view mode
|
|
151
|
+
*/
|
|
152
|
+
const expandToView = () => {
|
|
153
|
+
if (state.isExpanded) return;
|
|
154
|
+
|
|
155
|
+
state.isExpanded = true;
|
|
156
|
+
component.element.classList.add(`${component.getClass('search')}--expanded`);
|
|
157
|
+
|
|
158
|
+
// Apply view variant class if not already present
|
|
159
|
+
if (!component.element.classList.contains(`${component.getClass('search')}--view`)) {
|
|
160
|
+
component.element.classList.remove(`${component.getClass('search')}--bar`);
|
|
161
|
+
component.element.classList.add(`${component.getClass('search')}--view`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Show suggestions
|
|
165
|
+
showSuggestions();
|
|
166
|
+
|
|
167
|
+
// Focus input
|
|
168
|
+
if (input) {
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
input.focus();
|
|
171
|
+
}, 50);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Collapses view mode to search bar
|
|
177
|
+
*/
|
|
178
|
+
const collapseToBar = () => {
|
|
179
|
+
if (!state.isExpanded) return;
|
|
180
|
+
|
|
181
|
+
state.isExpanded = false;
|
|
182
|
+
component.element.classList.remove(`${component.getClass('search')}--expanded`);
|
|
183
|
+
|
|
184
|
+
// Apply bar variant class if not already present
|
|
185
|
+
if (!component.element.classList.contains(`${component.getClass('search')}--bar`)) {
|
|
186
|
+
component.element.classList.remove(`${component.getClass('search')}--view`);
|
|
187
|
+
component.element.classList.add(`${component.getClass('search')}--bar`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Hide suggestions
|
|
191
|
+
hideSuggestions();
|
|
192
|
+
|
|
193
|
+
// Blur input
|
|
194
|
+
if (input) {
|
|
195
|
+
input.blur();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Renders suggestions in the suggestions container
|
|
201
|
+
*/
|
|
202
|
+
const renderSuggestions = () => {
|
|
203
|
+
if (!suggestionsContainer || !state.suggestions || !state.suggestions.length) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Clear previous suggestions
|
|
208
|
+
suggestionsContainer.innerHTML = '';
|
|
209
|
+
|
|
210
|
+
// Create a list for suggestions
|
|
211
|
+
const list = document.createElement('ul');
|
|
212
|
+
list.className = component.getClass('search-suggestions-list');
|
|
213
|
+
list.setAttribute('role', 'listbox');
|
|
214
|
+
|
|
215
|
+
// Add suggestions
|
|
216
|
+
state.suggestions.forEach((suggestion, index) => {
|
|
217
|
+
const item = document.createElement('li');
|
|
218
|
+
item.className = component.getClass('search-suggestion-item');
|
|
219
|
+
item.setAttribute('role', 'option');
|
|
220
|
+
item.tabIndex = 0;
|
|
221
|
+
|
|
222
|
+
// Determine if suggestion is a string or object
|
|
223
|
+
if (typeof suggestion === 'string') {
|
|
224
|
+
item.textContent = suggestion;
|
|
225
|
+
// Highlight matched text if current input is a substring
|
|
226
|
+
if (state.value && suggestion.toLowerCase().includes(state.value.toLowerCase())) {
|
|
227
|
+
const matchedIndex = suggestion.toLowerCase().indexOf(state.value.toLowerCase());
|
|
228
|
+
const beforeMatch = suggestion.slice(0, matchedIndex);
|
|
229
|
+
const match = suggestion.slice(matchedIndex, matchedIndex + state.value.length);
|
|
230
|
+
const afterMatch = suggestion.slice(matchedIndex + state.value.length);
|
|
231
|
+
|
|
232
|
+
item.innerHTML = `${beforeMatch}<strong>${match}</strong>${afterMatch}`;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
// Object with text, value, and optional icon
|
|
236
|
+
if (suggestion.icon) {
|
|
237
|
+
const iconElement = document.createElement('span');
|
|
238
|
+
iconElement.className = component.getClass('search-suggestion-icon');
|
|
239
|
+
iconElement.innerHTML = suggestion.icon;
|
|
240
|
+
item.appendChild(iconElement);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const textElement = document.createElement('span');
|
|
244
|
+
textElement.className = component.getClass('search-suggestion-text');
|
|
245
|
+
textElement.textContent = suggestion.text;
|
|
246
|
+
|
|
247
|
+
// Highlight matched text if current input is a substring
|
|
248
|
+
if (state.value && suggestion.text.toLowerCase().includes(state.value.toLowerCase())) {
|
|
249
|
+
const matchedIndex = suggestion.text.toLowerCase().indexOf(state.value.toLowerCase());
|
|
250
|
+
const beforeMatch = suggestion.text.slice(0, matchedIndex);
|
|
251
|
+
const match = suggestion.text.slice(matchedIndex, matchedIndex + state.value.length);
|
|
252
|
+
const afterMatch = suggestion.text.slice(matchedIndex + state.value.length);
|
|
253
|
+
|
|
254
|
+
textElement.innerHTML = `${beforeMatch}<strong>${match}</strong>${afterMatch}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
item.appendChild(textElement);
|
|
258
|
+
|
|
259
|
+
// Store value as data attribute
|
|
260
|
+
if (suggestion.value) {
|
|
261
|
+
item.dataset.value = suggestion.value;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Add click handler
|
|
266
|
+
item.addEventListener('click', (e) => {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
e.stopPropagation();
|
|
269
|
+
const selectedValue = item.dataset.value || (typeof suggestion === 'string' ? suggestion : suggestion.text);
|
|
270
|
+
updateValue(selectedValue);
|
|
271
|
+
submitSearch();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
list.appendChild(item);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Add divider if configured and not already present
|
|
278
|
+
if (config.showDividers && divider && divider.parentElement !== suggestionsContainer) {
|
|
279
|
+
const dividerClone = divider.cloneNode(true);
|
|
280
|
+
suggestionsContainer.appendChild(dividerClone);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
suggestionsContainer.appendChild(list);
|
|
284
|
+
|
|
285
|
+
// Show suggestions container
|
|
286
|
+
suggestionsContainer.classList.add(`${component.getClass('search-suggestions-container')}--visible`);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Sets up all event listeners
|
|
291
|
+
*/
|
|
292
|
+
const setupEventListeners = () => {
|
|
293
|
+
// Input events
|
|
294
|
+
if (input) {
|
|
295
|
+
// Input value change
|
|
296
|
+
input.addEventListener('input', (e) => {
|
|
297
|
+
updateValue(e.target.value);
|
|
298
|
+
|
|
299
|
+
// Show suggestions if expanded
|
|
300
|
+
if (state.isExpanded) {
|
|
301
|
+
showSuggestions();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Focus event
|
|
306
|
+
input.addEventListener('focus', (e) => {
|
|
307
|
+
state.isFocused = true;
|
|
308
|
+
component.element.classList.add(`${component.getClass('search')}--focused`);
|
|
309
|
+
|
|
310
|
+
// Expand search bar to view if in bar mode
|
|
311
|
+
if (!state.isExpanded && config.variant === SEARCH_VARIANTS.BAR) {
|
|
312
|
+
expandToView();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
eventHelpers.triggerEvent(SEARCH_EVENTS.FOCUS, e);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Blur event
|
|
319
|
+
input.addEventListener('blur', (e) => {
|
|
320
|
+
// Don't blur if clicking inside the search component
|
|
321
|
+
if (component.element.contains(e.relatedTarget)) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
state.isFocused = false;
|
|
326
|
+
component.element.classList.remove(`${component.getClass('search')}--focused`);
|
|
327
|
+
|
|
328
|
+
// Hide suggestions with slight delay to allow for clicks
|
|
329
|
+
setTimeout(() => {
|
|
330
|
+
if (!state.isFocused) {
|
|
331
|
+
hideSuggestions();
|
|
332
|
+
|
|
333
|
+
// Collapse to bar mode if in expanded state and originally a bar
|
|
334
|
+
if (state.isExpanded && config.variant === SEARCH_VARIANTS.BAR) {
|
|
335
|
+
collapseToBar();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}, 200);
|
|
339
|
+
|
|
340
|
+
eventHelpers.triggerEvent(SEARCH_EVENTS.BLUR, e);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Enter key for submit
|
|
344
|
+
input.addEventListener('keydown', (e) => {
|
|
345
|
+
if (e.key === 'Enter') {
|
|
346
|
+
e.preventDefault();
|
|
347
|
+
submitSearch();
|
|
348
|
+
} else if (e.key === 'Escape') {
|
|
349
|
+
e.preventDefault();
|
|
350
|
+
|
|
351
|
+
// Clear if there's a value, otherwise collapse
|
|
352
|
+
if (state.value) {
|
|
353
|
+
clearSearch();
|
|
354
|
+
} else if (state.isExpanded && config.variant === SEARCH_VARIANTS.BAR) {
|
|
355
|
+
collapseToBar();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Leading icon click
|
|
362
|
+
if (leadingIcon) {
|
|
363
|
+
leadingIcon.addEventListener('click', (e) => {
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
|
|
366
|
+
// If disabled, do nothing
|
|
367
|
+
if (component.disabled?.isDisabled()) return;
|
|
368
|
+
|
|
369
|
+
// Toggle between expanded and collapsed
|
|
370
|
+
if (state.isExpanded) {
|
|
371
|
+
collapseToBar();
|
|
372
|
+
} else {
|
|
373
|
+
expandToView();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
eventHelpers.triggerEvent(SEARCH_EVENTS.ICON_CLICK, e);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Keyboard access
|
|
380
|
+
leadingIcon.addEventListener('keydown', (e) => {
|
|
381
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
382
|
+
e.preventDefault();
|
|
383
|
+
leadingIcon.click();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Clear button click
|
|
389
|
+
if (clearButton) {
|
|
390
|
+
clearButton.addEventListener('click', (e) => {
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
|
|
393
|
+
// If disabled, do nothing
|
|
394
|
+
if (component.disabled?.isDisabled()) return;
|
|
395
|
+
|
|
396
|
+
clearSearch();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Keyboard access
|
|
400
|
+
clearButton.addEventListener('keydown', (e) => {
|
|
401
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
402
|
+
e.preventDefault();
|
|
403
|
+
clearButton.click();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Trailing icon click
|
|
409
|
+
if (trailingIcon) {
|
|
410
|
+
trailingIcon.addEventListener('click', (e) => {
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
|
|
413
|
+
// If disabled, do nothing
|
|
414
|
+
if (component.disabled?.isDisabled()) return;
|
|
415
|
+
|
|
416
|
+
eventHelpers.triggerEvent(SEARCH_EVENTS.ICON_CLICK, e);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Keyboard access
|
|
420
|
+
trailingIcon.addEventListener('keydown', (e) => {
|
|
421
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
422
|
+
e.preventDefault();
|
|
423
|
+
trailingIcon.click();
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Second trailing icon click
|
|
429
|
+
if (trailingIcon2) {
|
|
430
|
+
trailingIcon2.addEventListener('click', (e) => {
|
|
431
|
+
e.preventDefault();
|
|
432
|
+
|
|
433
|
+
// If disabled, do nothing
|
|
434
|
+
if (component.disabled?.isDisabled()) return;
|
|
435
|
+
|
|
436
|
+
eventHelpers.triggerEvent(SEARCH_EVENTS.ICON_CLICK, e);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Keyboard access
|
|
440
|
+
trailingIcon2.addEventListener('keydown', (e) => {
|
|
441
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
442
|
+
e.preventDefault();
|
|
443
|
+
trailingIcon2.click();
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Handle clicks outside to close suggestions
|
|
449
|
+
document.addEventListener('click', (e) => {
|
|
450
|
+
if (!component.element.contains(e.target) && state.isExpanded) {
|
|
451
|
+
hideSuggestions();
|
|
452
|
+
|
|
453
|
+
// Collapse to bar mode if in expanded state and originally a bar
|
|
454
|
+
if (state.isExpanded && config.variant === SEARCH_VARIANTS.BAR) {
|
|
455
|
+
collapseToBar();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Clean up all event listeners
|
|
463
|
+
*/
|
|
464
|
+
const cleanupEventListeners = () => {
|
|
465
|
+
// Nothing to do if component is already destroyed
|
|
466
|
+
if (!component.element) return;
|
|
467
|
+
|
|
468
|
+
// Document click listener cleanup
|
|
469
|
+
document.removeEventListener('click', (e) => {
|
|
470
|
+
if (!component.element.contains(e.target) && state.isExpanded) {
|
|
471
|
+
hideSuggestions();
|
|
472
|
+
|
|
473
|
+
// Collapse to bar mode if in expanded state and originally a bar
|
|
474
|
+
if (state.isExpanded && config.variant === SEARCH_VARIANTS.BAR) {
|
|
475
|
+
collapseToBar();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Initialize search component
|
|
482
|
+
const initSearch = () => {
|
|
483
|
+
// Set initial value if provided
|
|
484
|
+
if (config.value && input) {
|
|
485
|
+
input.value = config.value;
|
|
486
|
+
|
|
487
|
+
// Show clear button if value exists
|
|
488
|
+
if (clearButton && config.showClearButton !== false) {
|
|
489
|
+
clearButton.classList.remove(`${component.getClass('search-clear-button')}--hidden`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Set ARIA attributes
|
|
494
|
+
if (input) {
|
|
495
|
+
input.setAttribute('role', 'searchbox');
|
|
496
|
+
input.setAttribute('aria-label', state.placeholder || 'Search');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Setup event listeners
|
|
500
|
+
setupEventListeners();
|
|
501
|
+
|
|
502
|
+
// If in view mode, show suggestions
|
|
503
|
+
if (state.isExpanded && config.suggestions && config.suggestions.length > 0) {
|
|
504
|
+
showSuggestions();
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Register with lifecycle if available
|
|
509
|
+
if (component.lifecycle) {
|
|
510
|
+
const originalDestroy = component.lifecycle.destroy || (() => {});
|
|
511
|
+
component.lifecycle.destroy = () => {
|
|
512
|
+
cleanupEventListeners();
|
|
513
|
+
originalDestroy();
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Initialize search
|
|
518
|
+
initSearch();
|
|
519
|
+
|
|
520
|
+
// Return enhanced component
|
|
521
|
+
return {
|
|
522
|
+
...component,
|
|
523
|
+
search: {
|
|
524
|
+
/**
|
|
525
|
+
* Sets search value
|
|
526
|
+
* @param value New value
|
|
527
|
+
* @param triggerEvent Whether to trigger change event
|
|
528
|
+
* @returns Search controller for chaining
|
|
529
|
+
*/
|
|
530
|
+
setValue(value, triggerEvent = true) {
|
|
531
|
+
updateValue(value, triggerEvent);
|
|
532
|
+
return this;
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Gets search value
|
|
537
|
+
* @returns Current value
|
|
538
|
+
*/
|
|
539
|
+
getValue() {
|
|
540
|
+
return state.value;
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Sets placeholder text
|
|
545
|
+
* @param text New placeholder text
|
|
546
|
+
* @returns Search controller for chaining
|
|
547
|
+
*/
|
|
548
|
+
setPlaceholder(text) {
|
|
549
|
+
state.placeholder = text;
|
|
550
|
+
if (input) {
|
|
551
|
+
input.placeholder = text;
|
|
552
|
+
input.setAttribute('aria-label', text);
|
|
553
|
+
}
|
|
554
|
+
return this;
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Gets placeholder text
|
|
559
|
+
* @returns Current placeholder
|
|
560
|
+
*/
|
|
561
|
+
getPlaceholder() {
|
|
562
|
+
return state.placeholder;
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Focuses the search input
|
|
567
|
+
* @returns Search controller for chaining
|
|
568
|
+
*/
|
|
569
|
+
focus() {
|
|
570
|
+
if (input && !component.disabled?.isDisabled()) {
|
|
571
|
+
input.focus();
|
|
572
|
+
}
|
|
573
|
+
return this;
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Blurs the search input
|
|
578
|
+
* @returns Search controller for chaining
|
|
579
|
+
*/
|
|
580
|
+
blur() {
|
|
581
|
+
if (input) {
|
|
582
|
+
input.blur();
|
|
583
|
+
}
|
|
584
|
+
return this;
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Expands the search bar into view mode
|
|
589
|
+
* @returns Search controller for chaining
|
|
590
|
+
*/
|
|
591
|
+
expand() {
|
|
592
|
+
expandToView();
|
|
593
|
+
return this;
|
|
594
|
+
},
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Collapses the search view back to bar mode
|
|
598
|
+
* @returns Search controller for chaining
|
|
599
|
+
*/
|
|
600
|
+
collapse() {
|
|
601
|
+
collapseToBar();
|
|
602
|
+
return this;
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Clears the search input
|
|
607
|
+
* @returns Search controller for chaining
|
|
608
|
+
*/
|
|
609
|
+
clear() {
|
|
610
|
+
clearSearch();
|
|
611
|
+
return this;
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Submits the search
|
|
616
|
+
* @returns Search controller for chaining
|
|
617
|
+
*/
|
|
618
|
+
submit() {
|
|
619
|
+
submitSearch();
|
|
620
|
+
return this;
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Sets suggestions
|
|
625
|
+
* @param suggestions Array of suggestions
|
|
626
|
+
* @returns Search controller for chaining
|
|
627
|
+
*/
|
|
628
|
+
setSuggestions(suggestions) {
|
|
629
|
+
state.suggestions = suggestions;
|
|
630
|
+
if (state.isExpanded) {
|
|
631
|
+
renderSuggestions();
|
|
632
|
+
}
|
|
633
|
+
return this;
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Shows or hides suggestions
|
|
638
|
+
* @param show Whether to show suggestions
|
|
639
|
+
* @returns Search controller for chaining
|
|
640
|
+
*/
|
|
641
|
+
showSuggestions(show) {
|
|
642
|
+
if (show) {
|
|
643
|
+
showSuggestions();
|
|
644
|
+
} else {
|
|
645
|
+
hideSuggestions();
|
|
646
|
+
}
|
|
647
|
+
return this;
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
// Icon management - separate from appearance to make API cleaner
|
|
652
|
+
icons: {
|
|
653
|
+
/**
|
|
654
|
+
* Sets leading icon
|
|
655
|
+
* @param iconHtml HTML content for icon
|
|
656
|
+
* @returns Icon manager for chaining
|
|
657
|
+
*/
|
|
658
|
+
setLeadingIcon(iconHtml) {
|
|
659
|
+
if (leadingIcon) {
|
|
660
|
+
leadingIcon.innerHTML = iconHtml || '';
|
|
661
|
+
}
|
|
662
|
+
return this;
|
|
663
|
+
},
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Sets trailing icon
|
|
667
|
+
* @param iconHtml HTML content for icon
|
|
668
|
+
* @returns Icon manager for chaining
|
|
669
|
+
*/
|
|
670
|
+
setTrailingIcon(iconHtml) {
|
|
671
|
+
if (trailingIcon) {
|
|
672
|
+
trailingIcon.innerHTML = iconHtml || '';
|
|
673
|
+
}
|
|
674
|
+
return this;
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Sets second trailing icon
|
|
679
|
+
* @param iconHtml HTML content for icon
|
|
680
|
+
* @returns Icon manager for chaining
|
|
681
|
+
*/
|
|
682
|
+
setTrailingIcon2(iconHtml) {
|
|
683
|
+
if (trailingIcon2) {
|
|
684
|
+
trailingIcon2.innerHTML = iconHtml || '';
|
|
685
|
+
}
|
|
686
|
+
return this;
|
|
687
|
+
},
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Sets avatar
|
|
691
|
+
* @param avatarHtml HTML content for avatar
|
|
692
|
+
* @returns Icon manager for chaining
|
|
693
|
+
*/
|
|
694
|
+
setAvatar(avatarHtml) {
|
|
695
|
+
if (avatar) {
|
|
696
|
+
avatar.innerHTML = avatarHtml || '';
|
|
697
|
+
}
|
|
698
|
+
return this;
|
|
699
|
+
},
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Shows or hides clear button
|
|
703
|
+
* @param show Whether to show clear button
|
|
704
|
+
* @returns Icon manager for chaining
|
|
705
|
+
*/
|
|
706
|
+
showClearButton(show) {
|
|
707
|
+
if (clearButton) {
|
|
708
|
+
if (show) {
|
|
709
|
+
clearButton.classList.remove(`${component.getClass('search-clear-button')}--hidden`);
|
|
710
|
+
} else {
|
|
711
|
+
clearButton.classList.add(`${component.getClass('search-clear-button')}--hidden`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return this;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
};
|