@rettangoli/ui 0.1.2-rc26 → 0.1.2-rc28
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/rettangoli-iife-layout.min.js +13 -12
- package/dist/rettangoli-iife-ui.min.js +18 -17
- package/package.json +1 -1
- package/src/components/form/form.handlers.js +14 -0
- package/src/components/form/form.view.yaml +11 -4
- package/src/components/select/select.handlers.js +92 -8
- package/src/components/select/select.store.js +70 -8
- package/src/components/select/select.view.yaml +39 -9
- package/src/primitives/svg.js +2 -0
package/package.json
CHANGED
|
@@ -186,3 +186,17 @@ export const handleWaveformClick = (e, deps) => {
|
|
|
186
186
|
}),
|
|
187
187
|
);
|
|
188
188
|
};
|
|
189
|
+
|
|
190
|
+
export const handleSelectAddOption = (e, deps) => {
|
|
191
|
+
const { store, dispatchEvent } = deps;
|
|
192
|
+
const name = e.currentTarget.id.replace("select-", "");
|
|
193
|
+
dispatchEvent(
|
|
194
|
+
new CustomEvent("action-click", {
|
|
195
|
+
detail: {
|
|
196
|
+
actionId: 'select-options-add',
|
|
197
|
+
name: name,
|
|
198
|
+
formValues: store.selectFormValues(),
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
};
|
|
@@ -50,19 +50,23 @@ propsSchema:
|
|
|
50
50
|
const: select
|
|
51
51
|
placeholder:
|
|
52
52
|
type: string
|
|
53
|
+
noClear:
|
|
54
|
+
type: boolean
|
|
55
|
+
addOption:
|
|
56
|
+
type: object
|
|
57
|
+
properties:
|
|
58
|
+
label:
|
|
59
|
+
type: string
|
|
53
60
|
options:
|
|
54
61
|
type: array
|
|
55
62
|
items:
|
|
56
63
|
type: object
|
|
57
64
|
properties:
|
|
58
|
-
id:
|
|
59
|
-
type: string
|
|
60
65
|
label:
|
|
61
66
|
type: string
|
|
62
67
|
value:
|
|
63
68
|
type: any
|
|
64
69
|
required:
|
|
65
|
-
- id
|
|
66
70
|
- label
|
|
67
71
|
- value
|
|
68
72
|
required:
|
|
@@ -209,6 +213,8 @@ refs:
|
|
|
209
213
|
eventListeners:
|
|
210
214
|
select-change:
|
|
211
215
|
handler: handleSelectChange
|
|
216
|
+
add-option-selected:
|
|
217
|
+
handler: handleSelectAddOption
|
|
212
218
|
colorpicker-*:
|
|
213
219
|
eventListeners:
|
|
214
220
|
colorpicker-change:
|
|
@@ -241,6 +247,7 @@ refs:
|
|
|
241
247
|
events:
|
|
242
248
|
form-change: {}
|
|
243
249
|
extra-event: {}
|
|
250
|
+
action-click: {}
|
|
244
251
|
|
|
245
252
|
template:
|
|
246
253
|
- rtgl-view w=f p=md g=lg ${containerAttrString}:
|
|
@@ -260,7 +267,7 @@ template:
|
|
|
260
267
|
- $if field.inputType == "popover-input":
|
|
261
268
|
- rtgl-popover-input#popover-input-${field.name} label="${field.label}" .defaultValue=fields[${i}].defaultValue:
|
|
262
269
|
- $if field.inputType == "select":
|
|
263
|
-
- rtgl-select#select-${field.name} key=${key} w=f .options=fields[${i}].options .placeholder=fields[${i}].placeholder .selectedValue=fields[${i}].defaultValue:
|
|
270
|
+
- rtgl-select#select-${field.name} key=${key} w=f .options=fields[${i}].options .placeholder=fields[${i}].placeholder .selectedValue=fields[${i}].defaultValue ?no-clear=fields[${i}].noClear .addOption=fields[${i}].addOption:
|
|
264
271
|
- $if field.inputType == "colorPicker":
|
|
265
272
|
- rtgl-color-picker#colorpicker-${field.name} key=${key} value=${field.defaultValue}:
|
|
266
273
|
- $if field.inputType == "slider":
|
|
@@ -2,7 +2,7 @@ import { deepEqual } from '../../common.js';
|
|
|
2
2
|
|
|
3
3
|
export const handleBeforeMount = (deps) => {
|
|
4
4
|
const { store, props, render } = deps;
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
if (props.selectedValue !== null && props.selectedValue !== undefined && props.options) {
|
|
7
7
|
const selectedOption = props.options.find(opt => deepEqual(opt.value, props.selectedValue));
|
|
8
8
|
if (selectedOption) {
|
|
@@ -15,16 +15,16 @@ export const handleBeforeMount = (deps) => {
|
|
|
15
15
|
export const handleOnUpdate = (changes, deps) => {
|
|
16
16
|
const { oldAttrs, newAttrs, oldProps, newProps } = changes;
|
|
17
17
|
const { store, props, render } = deps;
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
// Check if key changed
|
|
20
20
|
if (oldAttrs?.key !== newAttrs?.key && newAttrs?.key) {
|
|
21
21
|
// Clear current state using store action
|
|
22
22
|
store.resetSelection();
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
// Re-apply the prop value if available
|
|
25
25
|
const selectedValue = newProps?.selectedValue || props?.selectedValue;
|
|
26
26
|
const options = newProps?.options || props?.options;
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
if (selectedValue !== null && selectedValue !== undefined && options) {
|
|
29
29
|
const selectedOption = options.find(opt => deepEqual(opt.value, selectedValue));
|
|
30
30
|
if (selectedOption) {
|
|
@@ -36,12 +36,29 @@ export const handleOnUpdate = (changes, deps) => {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export const handleButtonClick = (e, deps) => {
|
|
39
|
-
const { store, render, getRefIds } = deps;
|
|
39
|
+
const { store, render, getRefIds, props } = deps;
|
|
40
|
+
|
|
41
|
+
const button = getRefIds()['select-button'].elm;
|
|
42
|
+
|
|
43
|
+
// Get first child's bounding rectangle (since button has display: contents)
|
|
44
|
+
const firstChild = button.firstElementChild;
|
|
45
|
+
const rect = firstChild ? firstChild.getBoundingClientRect() : button.getBoundingClientRect();
|
|
46
|
+
|
|
47
|
+
// Find the index of the currently selected option
|
|
48
|
+
const storeSelectedValue = store.selectSelectedValue();
|
|
49
|
+
const currentValue = storeSelectedValue !== null ? storeSelectedValue : props.selectedValue;
|
|
50
|
+
let selectedIndex = null;
|
|
51
|
+
if (currentValue !== null && currentValue !== undefined && props.options) {
|
|
52
|
+
selectedIndex = props.options.findIndex(opt => deepEqual(opt.value, currentValue));
|
|
53
|
+
if (selectedIndex === -1) selectedIndex = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
store.openOptionsPopover({
|
|
41
57
|
position: {
|
|
42
|
-
y:
|
|
43
|
-
x:
|
|
44
|
-
}
|
|
58
|
+
y: rect.bottom + 12, // Bottom edge of button
|
|
59
|
+
x: rect.left - 24, // Left edge of button
|
|
60
|
+
},
|
|
61
|
+
selectedIndex
|
|
45
62
|
})
|
|
46
63
|
render();
|
|
47
64
|
}
|
|
@@ -77,6 +94,73 @@ export const handleOptionClick = (e, deps) => {
|
|
|
77
94
|
detail: { selectedValue: option.value },
|
|
78
95
|
bubbles: true
|
|
79
96
|
}));
|
|
97
|
+
|
|
98
|
+
render();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const handleOptionMouseEnter = (e, deps) => {
|
|
102
|
+
const { store, render } = deps;
|
|
103
|
+
const id = parseInt(e.currentTarget.id.replace('option-', ''));
|
|
104
|
+
store.setHoveredOption(id);
|
|
105
|
+
render();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const handleOptionMouseLeave = (e, deps) => {
|
|
109
|
+
const { store, render } = deps;
|
|
110
|
+
store.clearHoveredOption();
|
|
111
|
+
render();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const handleClearClick = (e, deps) => {
|
|
115
|
+
const { store, render, dispatchEvent, props } = deps;
|
|
116
|
+
|
|
117
|
+
e.stopPropagation();
|
|
118
|
+
|
|
119
|
+
// Clear the internal state
|
|
120
|
+
store.clearSelectedValue();
|
|
121
|
+
|
|
122
|
+
// Call onChange if provided
|
|
123
|
+
if (props.onChange && typeof props.onChange === 'function') {
|
|
124
|
+
props.onChange(undefined);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Dispatch custom event for backward compatibility
|
|
128
|
+
dispatchEvent(new CustomEvent('option-selected', {
|
|
129
|
+
detail: { value: undefined, label: undefined },
|
|
130
|
+
bubbles: true
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
// Also dispatch select-change event to match form's event listener pattern
|
|
134
|
+
dispatchEvent(new CustomEvent('select-change', {
|
|
135
|
+
detail: { selectedValue: undefined },
|
|
136
|
+
bubbles: true
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
render();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const handleAddOptionClick = (e, deps) => {
|
|
143
|
+
const { store, render, dispatchEvent } = deps;
|
|
80
144
|
|
|
145
|
+
// Close the popover
|
|
146
|
+
store.closeOptionsPopover();
|
|
147
|
+
|
|
148
|
+
// Dispatch custom event for add option (no detail)
|
|
149
|
+
dispatchEvent(new CustomEvent('add-option-selected', {
|
|
150
|
+
bubbles: true
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
render();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const handleAddOptionMouseEnter = (e, deps) => {
|
|
157
|
+
const { store, render } = deps;
|
|
158
|
+
store.setHoveredAddOption(true);
|
|
159
|
+
render();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const handleAddOptionMouseLeave = (e, deps) => {
|
|
163
|
+
const { store, render } = deps;
|
|
164
|
+
store.setHoveredAddOption(false);
|
|
81
165
|
render();
|
|
82
166
|
}
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { deepEqual } from '../../common.js';
|
|
2
2
|
|
|
3
|
+
// Attributes that should not be passed through to the container
|
|
4
|
+
// These are either handled internally or have special meaning
|
|
5
|
+
const blacklistedAttrs = [
|
|
6
|
+
"id",
|
|
7
|
+
"class",
|
|
8
|
+
"style",
|
|
9
|
+
"slot",
|
|
10
|
+
// Select-specific props that are handled separately
|
|
11
|
+
"placeholder",
|
|
12
|
+
"selectedValue",
|
|
13
|
+
"selected-value",
|
|
14
|
+
"onChange",
|
|
15
|
+
"on-change",
|
|
16
|
+
"options"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const stringifyAttrs = (attrs) => {
|
|
20
|
+
return Object.entries(attrs || {})
|
|
21
|
+
.filter(([key]) => !blacklistedAttrs.includes(key))
|
|
22
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
23
|
+
.join(" ");
|
|
24
|
+
};
|
|
25
|
+
|
|
3
26
|
export const INITIAL_STATE = Object.freeze({
|
|
4
27
|
isOpen: false,
|
|
5
28
|
position: {
|
|
@@ -7,38 +30,53 @@ export const INITIAL_STATE = Object.freeze({
|
|
|
7
30
|
y: 0,
|
|
8
31
|
},
|
|
9
32
|
selectedValue: null,
|
|
33
|
+
hoveredOptionId: null,
|
|
34
|
+
hoveredAddOption: false,
|
|
10
35
|
});
|
|
11
36
|
|
|
12
|
-
export const toViewData = ({ state, props }) => {
|
|
37
|
+
export const toViewData = ({ state, props, attrs }) => {
|
|
38
|
+
// Generate container attribute string
|
|
39
|
+
const containerAttrString = stringifyAttrs(attrs);
|
|
40
|
+
|
|
13
41
|
// Use state's selected value if available, otherwise use props.selectedValue
|
|
14
42
|
const currentValue = state.selectedValue !== null ? state.selectedValue : props.selectedValue;
|
|
15
|
-
|
|
43
|
+
|
|
16
44
|
// Calculate display label from value
|
|
17
45
|
let displayLabel = props.placeholder || 'Select an option';
|
|
46
|
+
let isPlaceholderLabel = true;
|
|
18
47
|
if (currentValue !== null && currentValue !== undefined && props.options) {
|
|
19
48
|
const selectedOption = props.options.find(opt => deepEqual(opt.value, currentValue));
|
|
20
49
|
if (selectedOption) {
|
|
21
50
|
displayLabel = selectedOption.label;
|
|
51
|
+
isPlaceholderLabel = false;
|
|
22
52
|
}
|
|
23
53
|
}
|
|
24
|
-
|
|
54
|
+
|
|
25
55
|
// Map options to include isSelected flag and computed background color
|
|
26
|
-
const optionsWithSelection = (props.options || []).map(option => {
|
|
56
|
+
const optionsWithSelection = (props.options || []).map((option, index) => {
|
|
27
57
|
const isSelected = deepEqual(option.value, currentValue);
|
|
58
|
+
const isHovered = state.hoveredOptionId === index;
|
|
28
59
|
return {
|
|
29
60
|
...option,
|
|
30
61
|
isSelected,
|
|
31
|
-
bgc: isSelected ? 'mu' : ''
|
|
62
|
+
bgc: isHovered ? 'ac' : (isSelected ? 'mu' : '')
|
|
32
63
|
};
|
|
33
64
|
});
|
|
34
|
-
|
|
65
|
+
|
|
35
66
|
return {
|
|
67
|
+
containerAttrString,
|
|
36
68
|
isOpen: state.isOpen,
|
|
37
69
|
position: state.position,
|
|
38
70
|
options: optionsWithSelection,
|
|
39
71
|
selectedValue: currentValue,
|
|
40
72
|
selectedLabel: displayLabel,
|
|
41
|
-
|
|
73
|
+
selectedLabelColor: isPlaceholderLabel ? "mu-fg" : "fg",
|
|
74
|
+
placeholder: props.placeholder || 'Select an option',
|
|
75
|
+
hasValue: currentValue !== null && currentValue !== undefined,
|
|
76
|
+
showClear: !attrs['no-clear'] && !props['no-clear'] && (currentValue !== null && currentValue !== undefined),
|
|
77
|
+
showAddOption: !!props.addOption,
|
|
78
|
+
addOptionLabel: props.addOption?.label ? `+ ${props.addOption.label}` : '+ Add',
|
|
79
|
+
addOptionBgc: state.hoveredAddOption ? 'ac' : ''
|
|
42
80
|
};
|
|
43
81
|
}
|
|
44
82
|
|
|
@@ -46,10 +84,18 @@ export const selectState = ({ state }) => {
|
|
|
46
84
|
return state;
|
|
47
85
|
}
|
|
48
86
|
|
|
87
|
+
export const selectSelectedValue = ({ state }) => {
|
|
88
|
+
return state.selectedValue;
|
|
89
|
+
}
|
|
90
|
+
|
|
49
91
|
export const openOptionsPopover = (state, payload) => {
|
|
50
|
-
const { position } = payload;
|
|
92
|
+
const { position, selectedIndex } = payload;
|
|
51
93
|
state.position = position;
|
|
52
94
|
state.isOpen = true;
|
|
95
|
+
// Set hoveredOptionId to the selected option's index if available
|
|
96
|
+
if (selectedIndex !== undefined && selectedIndex !== null) {
|
|
97
|
+
state.hoveredOptionId = selectedIndex;
|
|
98
|
+
}
|
|
53
99
|
}
|
|
54
100
|
|
|
55
101
|
export const closeOptionsPopover = (state) => {
|
|
@@ -65,5 +111,21 @@ export const resetSelection = (state) => {
|
|
|
65
111
|
state.selectedValue = undefined;
|
|
66
112
|
}
|
|
67
113
|
|
|
114
|
+
export const setHoveredOption = (state, optionId) => {
|
|
115
|
+
state.hoveredOptionId = optionId;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const clearHoveredOption = (state) => {
|
|
119
|
+
state.hoveredOptionId = null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const clearSelectedValue = (state) => {
|
|
123
|
+
state.selectedValue = undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const setHoveredAddOption = (state, isHovered) => {
|
|
127
|
+
state.hoveredAddOption = isHovered;
|
|
128
|
+
}
|
|
129
|
+
|
|
68
130
|
|
|
69
131
|
|
|
@@ -11,8 +11,6 @@ propsSchema:
|
|
|
11
11
|
items:
|
|
12
12
|
type: object
|
|
13
13
|
properties:
|
|
14
|
-
id:
|
|
15
|
-
type: string
|
|
16
14
|
label:
|
|
17
15
|
type: string
|
|
18
16
|
value:
|
|
@@ -23,12 +21,23 @@ propsSchema:
|
|
|
23
21
|
type: string
|
|
24
22
|
onChange:
|
|
25
23
|
type: function
|
|
24
|
+
no-clear:
|
|
25
|
+
type: boolean
|
|
26
|
+
addOption:
|
|
27
|
+
type: object
|
|
28
|
+
properties:
|
|
29
|
+
label:
|
|
30
|
+
type: string
|
|
26
31
|
|
|
27
32
|
refs:
|
|
28
33
|
select-button:
|
|
29
34
|
eventListeners:
|
|
30
35
|
click:
|
|
31
36
|
handler: handleButtonClick
|
|
37
|
+
clear-button:
|
|
38
|
+
eventListeners:
|
|
39
|
+
click:
|
|
40
|
+
handler: handleClearClick
|
|
32
41
|
popover:
|
|
33
42
|
eventListeners:
|
|
34
43
|
close:
|
|
@@ -37,14 +46,35 @@ refs:
|
|
|
37
46
|
eventListeners:
|
|
38
47
|
click:
|
|
39
48
|
handler: handleOptionClick
|
|
49
|
+
mouseenter:
|
|
50
|
+
handler: handleOptionMouseEnter
|
|
51
|
+
mouseleave:
|
|
52
|
+
handler: handleOptionMouseLeave
|
|
53
|
+
option-add:
|
|
54
|
+
eventListeners:
|
|
55
|
+
click:
|
|
56
|
+
handler: handleAddOptionClick
|
|
57
|
+
mouseenter:
|
|
58
|
+
handler: handleAddOptionMouseEnter
|
|
59
|
+
mouseleave:
|
|
60
|
+
handler: handleAddOptionMouseLeave
|
|
40
61
|
|
|
41
62
|
events: {}
|
|
42
63
|
|
|
43
64
|
template:
|
|
44
|
-
- rtgl-button#select-button v=ol:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- rtgl-
|
|
65
|
+
- rtgl-button#select-button v=ol ${containerAttrString}:
|
|
66
|
+
- rtgl-view d=h av=c w=f:
|
|
67
|
+
- rtgl-text c=${selectedLabelColor}: ${selectedLabel}
|
|
68
|
+
- rtgl-view mh=md flex=1:
|
|
69
|
+
- $if showClear:
|
|
70
|
+
- rtgl-svg#clear-button mr=md svg=x wh=16 c=mu-fg cur=p:
|
|
71
|
+
- rtgl-svg svg=chevronDown wh=16 c=mu-fg:
|
|
72
|
+
- rtgl-popover#popover ?open=${isOpen} x=${position.x} y=${position.y} placement=right-start:
|
|
73
|
+
- rtgl-view wh=300 g=xs slot=content bgc=background br=md sv=true:
|
|
74
|
+
- $for option, i in options:
|
|
75
|
+
- rtgl-view#option-${i} w=f ph=lg pv=md cur=p br=md bgc=${option.bgc}:
|
|
76
|
+
- rtgl-text: ${option.label}
|
|
77
|
+
- $if showAddOption:
|
|
78
|
+
- rtgl-view w=f bw=xs bc=mu-bg bt=sm:
|
|
79
|
+
- rtgl-view#option-add w=f ph=lg pv=md cur=p br=md bgc=${addOptionBgc}:
|
|
80
|
+
- rtgl-text c=ac: ${addOptionLabel}
|
package/src/primitives/svg.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { css, dimensionWithUnit } from "../common.js";
|
|
2
2
|
import flexChildStyles from "../styles/flexChildStyles.js";
|
|
3
3
|
import paddingSvgStyles from "../styles/paddingSvgStyles.js";
|
|
4
|
+
import marginStyles from "../styles/marginStyles.js";
|
|
4
5
|
import cursorStyles from "../styles/cursorStyles.js";
|
|
5
6
|
import textColorStyles from "../styles/textColorStyles.js";
|
|
6
7
|
|
|
@@ -18,6 +19,7 @@ class RettangoliSvgElement extends HTMLElement {
|
|
|
18
19
|
}
|
|
19
20
|
${textColorStyles}
|
|
20
21
|
${paddingSvgStyles}
|
|
22
|
+
${marginStyles}
|
|
21
23
|
${flexChildStyles}
|
|
22
24
|
${cursorStyles}
|
|
23
25
|
`);
|