@rettangoli/ui 0.1.2-rc25 → 0.1.2-rc27

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/ui",
3
- "version": "0.1.2-rc25",
3
+ "version": "0.1.2-rc27",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
package/src/common.js CHANGED
@@ -180,6 +180,25 @@ function convertObjectToCssString(styleObject, selector = ':host') {
180
180
  return result;
181
181
  }
182
182
 
183
+ // Deep equality helper for comparing values (including objects)
184
+ export const deepEqual = (a, b) => {
185
+ if (a === b) return true;
186
+ if (a == null || b == null) return false;
187
+ if (typeof a !== 'object' || typeof b !== 'object') return false;
188
+
189
+ const keysA = Object.keys(a);
190
+ const keysB = Object.keys(b);
191
+
192
+ if (keysA.length !== keysB.length) return false;
193
+
194
+ for (const key of keysA) {
195
+ if (!keysB.includes(key)) return false;
196
+ if (!deepEqual(a[key], b[key])) return false;
197
+ }
198
+
199
+ return true;
200
+ };
201
+
183
202
  export {
184
203
  css,
185
204
  generateCSS,
@@ -50,19 +50,18 @@ propsSchema:
50
50
  const: select
51
51
  placeholder:
52
52
  type: string
53
+ noClear:
54
+ type: boolean
53
55
  options:
54
56
  type: array
55
57
  items:
56
58
  type: object
57
59
  properties:
58
- id:
59
- type: string
60
60
  label:
61
61
  type: string
62
62
  value:
63
63
  type: any
64
64
  required:
65
- - id
66
65
  - label
67
66
  - value
68
67
  required:
@@ -260,7 +259,7 @@ template:
260
259
  - $if field.inputType == "popover-input":
261
260
  - rtgl-popover-input#popover-input-${field.name} label="${field.label}" .defaultValue=fields[${i}].defaultValue:
262
261
  - $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:
262
+ - 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:
264
263
  - $if field.inputType == "colorPicker":
265
264
  - rtgl-color-picker#colorpicker-${field.name} key=${key} value=${field.defaultValue}:
266
265
  - $if field.inputType == "slider":
@@ -1,9 +1,10 @@
1
+ import { deepEqual } from '../../common.js';
1
2
 
2
3
  export const handleBeforeMount = (deps) => {
3
4
  const { store, props, render } = deps;
4
-
5
+
5
6
  if (props.selectedValue !== null && props.selectedValue !== undefined && props.options) {
6
- const selectedOption = props.options.find(opt => opt.value === props.selectedValue);
7
+ const selectedOption = props.options.find(opt => deepEqual(opt.value, props.selectedValue));
7
8
  if (selectedOption) {
8
9
  store.updateSelectOption(selectedOption);
9
10
  render();
@@ -14,18 +15,18 @@ export const handleBeforeMount = (deps) => {
14
15
  export const handleOnUpdate = (changes, deps) => {
15
16
  const { oldAttrs, newAttrs, oldProps, newProps } = changes;
16
17
  const { store, props, render } = deps;
17
-
18
+
18
19
  // Check if key changed
19
20
  if (oldAttrs?.key !== newAttrs?.key && newAttrs?.key) {
20
21
  // Clear current state using store action
21
22
  store.resetSelection();
22
-
23
+
23
24
  // Re-apply the prop value if available
24
25
  const selectedValue = newProps?.selectedValue || props?.selectedValue;
25
26
  const options = newProps?.options || props?.options;
26
-
27
+
27
28
  if (selectedValue !== null && selectedValue !== undefined && options) {
28
- const selectedOption = options.find(opt => opt.value === selectedValue);
29
+ const selectedOption = options.find(opt => deepEqual(opt.value, selectedValue));
29
30
  if (selectedOption) {
30
31
  store.updateSelectOption(selectedOption);
31
32
  }
@@ -35,12 +36,29 @@ export const handleOnUpdate = (changes, deps) => {
35
36
  }
36
37
 
37
38
  export const handleButtonClick = (e, deps) => {
38
- 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
+
39
56
  store.openOptionsPopover({
40
57
  position: {
41
- y: e.clientY,
42
- x: e.clientX,
43
- }
58
+ y: rect.bottom + 12, // Bottom edge of button
59
+ x: rect.left - 24, // Left edge of button
60
+ },
61
+ selectedIndex
44
62
  })
45
63
  render();
46
64
  }
@@ -76,6 +94,47 @@ export const handleOptionClick = (e, deps) => {
76
94
  detail: { selectedValue: option.value },
77
95
  bubbles: true
78
96
  }));
79
-
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
+
80
139
  render();
81
140
  }
@@ -1,3 +1,28 @@
1
+ import { deepEqual } from '../../common.js';
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
+
1
26
  export const INITIAL_STATE = Object.freeze({
2
27
  isOpen: false,
3
28
  position: {
@@ -5,39 +30,49 @@ export const INITIAL_STATE = Object.freeze({
5
30
  y: 0,
6
31
  },
7
32
  selectedValue: null,
8
- selectedLabel: null,
33
+ hoveredOptionId: null,
9
34
  });
10
35
 
11
- export const toViewData = ({ state, props }) => {
12
- // Calculate display label
13
- let displayLabel = props.placeholder || 'Select an option';
36
+ export const toViewData = ({ state, props, attrs }) => {
37
+ // Generate container attribute string
38
+ const containerAttrString = stringifyAttrs(attrs);
14
39
 
15
40
  // Use state's selected value if available, otherwise use props.selectedValue
16
41
  const currentValue = state.selectedValue !== null ? state.selectedValue : props.selectedValue;
17
-
42
+
43
+ // Calculate display label from value
44
+ let displayLabel = props.placeholder || 'Select an option';
45
+ let isPlaceholderLabel = true;
18
46
  if (currentValue !== null && currentValue !== undefined && props.options) {
19
- const selectedOption = props.options.find(opt => opt.value === currentValue);
47
+ const selectedOption = props.options.find(opt => deepEqual(opt.value, currentValue));
20
48
  if (selectedOption) {
21
49
  displayLabel = selectedOption.label;
50
+ isPlaceholderLabel = false;
22
51
  }
23
- } else if (state.selectedLabel) {
24
- displayLabel = state.selectedLabel;
25
52
  }
26
-
53
+
27
54
  // Map options to include isSelected flag and computed background color
28
- const optionsWithSelection = (props.options || []).map(option => ({
29
- ...option,
30
- isSelected: option.value === currentValue,
31
- bgc: option.value === currentValue ? 'mu' : ''
32
- }));
33
-
55
+ const optionsWithSelection = (props.options || []).map((option, index) => {
56
+ const isSelected = deepEqual(option.value, currentValue);
57
+ const isHovered = state.hoveredOptionId === index;
58
+ return {
59
+ ...option,
60
+ isSelected,
61
+ bgc: isHovered ? 'ac' : (isSelected ? 'mu' : '')
62
+ };
63
+ });
64
+
34
65
  return {
66
+ containerAttrString,
35
67
  isOpen: state.isOpen,
36
68
  position: state.position,
37
69
  options: optionsWithSelection,
38
70
  selectedValue: currentValue,
39
71
  selectedLabel: displayLabel,
40
- placeholder: props.placeholder || 'Select an option'
72
+ selectedLabelColor: isPlaceholderLabel ? "mu-fg" : "fg",
73
+ placeholder: props.placeholder || 'Select an option',
74
+ hasValue: currentValue !== null && currentValue !== undefined,
75
+ showClear: !attrs['no-clear'] && !props['no-clear'] && (currentValue !== null && currentValue !== undefined)
41
76
  };
42
77
  }
43
78
 
@@ -45,10 +80,18 @@ export const selectState = ({ state }) => {
45
80
  return state;
46
81
  }
47
82
 
83
+ export const selectSelectedValue = ({ state }) => {
84
+ return state.selectedValue;
85
+ }
86
+
48
87
  export const openOptionsPopover = (state, payload) => {
49
- const { position } = payload;
88
+ const { position, selectedIndex } = payload;
50
89
  state.position = position;
51
90
  state.isOpen = true;
91
+ // Set hoveredOptionId to the selected option's index if available
92
+ if (selectedIndex !== undefined && selectedIndex !== null) {
93
+ state.hoveredOptionId = selectedIndex;
94
+ }
52
95
  }
53
96
 
54
97
  export const closeOptionsPopover = (state) => {
@@ -57,13 +100,23 @@ export const closeOptionsPopover = (state) => {
57
100
 
58
101
  export const updateSelectOption = (state, option) => {
59
102
  state.selectedValue = option.value;
60
- state.selectedLabel = option.label;
61
103
  state.isOpen = false;
62
104
  }
63
105
 
64
106
  export const resetSelection = (state) => {
65
107
  state.selectedValue = undefined;
66
- state.selectedLabel = undefined;
108
+ }
109
+
110
+ export const setHoveredOption = (state, optionId) => {
111
+ state.hoveredOptionId = optionId;
112
+ }
113
+
114
+ export const clearHoveredOption = (state) => {
115
+ state.hoveredOptionId = null;
116
+ }
117
+
118
+ export const clearSelectedValue = (state) => {
119
+ state.selectedValue = undefined;
67
120
  }
68
121
 
69
122
 
@@ -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,18 @@ propsSchema:
23
21
  type: string
24
22
  onChange:
25
23
  type: function
24
+ no-clear:
25
+ type: boolean
26
26
 
27
27
  refs:
28
28
  select-button:
29
29
  eventListeners:
30
30
  click:
31
31
  handler: handleButtonClick
32
+ clear-button:
33
+ eventListeners:
34
+ click:
35
+ handler: handleClearClick
32
36
  popover:
33
37
  eventListeners:
34
38
  close:
@@ -37,14 +41,23 @@ refs:
37
41
  eventListeners:
38
42
  click:
39
43
  handler: handleOptionClick
44
+ mouseenter:
45
+ handler: handleOptionMouseEnter
46
+ mouseleave:
47
+ handler: handleOptionMouseLeave
40
48
 
41
49
  events: {}
42
50
 
43
51
  template:
44
- - rtgl-button#select-button v=ol:
45
- - ${selectedLabel}
46
- - rtgl-popover#popover ?open=${isOpen} x=${position.x} y=${position.y}:
47
- - rtgl-view wh=300 g=xs slot=content bgc=background br=md sv=true:
48
- - $for option, i in options:
49
- - rtgl-view#option-${i} w=f h-bgc=ac ph=lg pv=md cur=p br=md bgc=${option.bgc}:
50
- - rtgl-text: ${option.label}
52
+ - rtgl-button#select-button v=ol ${containerAttrString}:
53
+ - rtgl-view d=h av=c w=f:
54
+ - rtgl-text c=${selectedLabelColor}: ${selectedLabel}
55
+ - rtgl-view mh=md flex=1:
56
+ - $if showClear:
57
+ - rtgl-svg#clear-button mr=md svg=x wh=16 c=mu-fg cur=p:
58
+ - rtgl-svg svg=chevronDown wh=16 c=mu-fg:
59
+ - rtgl-popover#popover ?open=${isOpen} x=${position.x} y=${position.y} placement=right-start:
60
+ - rtgl-view wh=300 g=xs slot=content bgc=background br=md sv=true:
61
+ - $for option, i in options:
62
+ - rtgl-view#option-${i} w=f ph=lg pv=md cur=p br=md bgc=${option.bgc}:
63
+ - rtgl-text: ${option.label}
@@ -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
  `);