@rettangoli/ui 1.7.0 → 1.7.2

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": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -75,7 +75,11 @@ export const handleOnUpdate = (deps, payload) => {
75
75
  }
76
76
 
77
77
  if (oldProps.options !== newProps.options) {
78
- shouldRender = true;
78
+ const hasCurrentValues = resolveCurrentValues({ store, props: newProps }).length > 0;
79
+
80
+ if (store.getState().isOpen || hasCurrentValues) {
81
+ shouldRender = true;
82
+ }
79
83
  }
80
84
 
81
85
  if (shouldRender) {
@@ -1,3 +1,5 @@
1
+ import { deepEqual } from "../../common.js";
2
+
1
3
  const resolvePopoverPosition = (trigger) => {
2
4
  if (!trigger || typeof trigger.getBoundingClientRect !== "function") {
3
5
  return null;
@@ -11,12 +13,66 @@ const resolvePopoverPosition = (trigger) => {
11
13
  };
12
14
  };
13
15
 
14
- export const refreshPopover = function () {
16
+ const normalizeSelectedValues = (selectedValues) => {
17
+ if (!Array.isArray(selectedValues)) {
18
+ return [];
19
+ }
20
+
21
+ return [...selectedValues];
22
+ };
23
+
24
+ const renderInstance = (instance) => {
25
+ if (typeof instance.render === "function") {
26
+ instance.render();
27
+ }
28
+ };
29
+
30
+ const resolveCurrentValues = (instance) => {
31
+ if (instance.store?.selectHasSelectedValues?.()) {
32
+ return instance.store?.selectSelectedValues?.() || [];
33
+ }
34
+
35
+ return normalizeSelectedValues(instance.props?.selectedValues);
36
+ };
37
+
38
+ const openPopoverWithDraft = (instance, values = []) => {
39
+ const position = resolvePopoverPosition(instance.refs?.trigger);
40
+
41
+ if (!position || !instance.store?.openOptionsPopover) {
42
+ return false;
43
+ }
44
+
45
+ instance.store.openOptionsPopover({
46
+ position,
47
+ values,
48
+ });
49
+
50
+ return true;
51
+ };
52
+
53
+ const setDraftValues = (instance, values = [], keepOpen = false) => {
54
+ const state = instance.store?.getState?.();
55
+
56
+ if (state?.isOpen && instance.store?.updateDraftSelectedValues) {
57
+ instance.store.updateDraftSelectedValues({ values });
58
+ return true;
59
+ }
60
+
61
+ if (keepOpen) {
62
+ return openPopoverWithDraft(instance, values);
63
+ }
64
+
65
+ return false;
66
+ };
67
+
68
+ export const refreshPopover = function (payload = {}) {
15
69
  const state = this.store?.getState?.();
70
+ const draftValues = Array.isArray(payload.values)
71
+ ? normalizeSelectedValues(payload.values)
72
+ : (this.store?.selectDraftSelectedValues?.() || []);
16
73
 
17
74
  if (state?.isOpen) {
18
75
  const position = resolvePopoverPosition(this.refs?.trigger);
19
- const draftValues = this.store?.selectDraftSelectedValues?.() || [];
20
76
 
21
77
  if (position) {
22
78
  this.store.openOptionsPopover({
@@ -24,9 +80,46 @@ export const refreshPopover = function () {
24
80
  values: draftValues,
25
81
  });
26
82
  }
83
+ } else if (payload.keepOpen) {
84
+ openPopoverWithDraft(this, draftValues.length > 0 ? draftValues : resolveCurrentValues(this));
27
85
  }
28
86
 
29
- if (typeof this.render === "function") {
30
- this.render();
87
+ renderInstance(this);
88
+ };
89
+
90
+ export const setDraftSelectedValues = function (payload = {}) {
91
+ const values = normalizeSelectedValues(payload.values);
92
+
93
+ if (!setDraftValues(this, values, !!payload.keepOpen)) {
94
+ return;
95
+ }
96
+
97
+ renderInstance(this);
98
+ };
99
+
100
+ export const appendDraftSelectedValue = function (payload = {}) {
101
+ if (!Object.prototype.hasOwnProperty.call(payload || {}, "value")) {
102
+ return;
31
103
  }
104
+
105
+ const state = this.store?.getState?.();
106
+ const currentValues = state?.isOpen
107
+ ? (this.store?.selectDraftSelectedValues?.() || [])
108
+ : resolveCurrentValues(this);
109
+
110
+ if (currentValues.some((currentValue) => deepEqual(currentValue, payload.value))) {
111
+ if (!!payload.keepOpen && !state?.isOpen && openPopoverWithDraft(this, currentValues)) {
112
+ renderInstance(this);
113
+ }
114
+
115
+ return;
116
+ }
117
+
118
+ const nextValues = [...normalizeSelectedValues(currentValues), payload.value];
119
+
120
+ if (!setDraftValues(this, nextValues, !!payload.keepOpen)) {
121
+ return;
122
+ }
123
+
124
+ renderInstance(this);
32
125
  };
@@ -48,4 +48,21 @@ events:
48
48
  methods:
49
49
  type: object
50
50
  properties:
51
- refreshPopover: {}
51
+ refreshPopover:
52
+ description: Recomputes the open popover position and rerenders the current options and draft values.
53
+ params:
54
+ - values
55
+ - keepOpen
56
+ returns: void
57
+ setDraftSelectedValues:
58
+ description: Replaces the draft-only selection without committing `selectedValues`; optionally reopens the popover around the trigger when `keepOpen` is true.
59
+ params:
60
+ - values
61
+ - keepOpen
62
+ returns: void
63
+ appendDraftSelectedValue:
64
+ description: Adds a value to the draft-only selection without committing `selectedValues`; optionally reopens the popover around the trigger when `keepOpen` is true.
65
+ params:
66
+ - value
67
+ - keepOpen
68
+ returns: void
@@ -68,26 +68,6 @@ const sameValueArray = (left = [], right = []) => {
68
68
  return left.every((value, index) => deepEqual(value, right[index]));
69
69
  };
70
70
 
71
- const stringifyKeyPart = (value) => {
72
- if (value === undefined) {
73
- return "undefined";
74
- }
75
-
76
- if (value === null) {
77
- return "null";
78
- }
79
-
80
- if (typeof value === "string") {
81
- return value;
82
- }
83
-
84
- try {
85
- return JSON.stringify(value);
86
- } catch {
87
- return String(value);
88
- }
89
- };
90
-
91
71
  const isSelectedValue = (selectedValues = [], candidate) => {
92
72
  return selectedValues.some((value) => deepEqual(value, candidate));
93
73
  };
@@ -133,37 +113,6 @@ const buildTagStyle = ({ isSelected = true, isAddChip = false } = {}) => {
133
113
  return `${baseStyle.join("; ")};`;
134
114
  };
135
115
 
136
- const buildPopoverSignature = (options = []) => {
137
- if (!Array.isArray(options) || options.length === 0) {
138
- return "empty";
139
- }
140
-
141
- return options.map((option, index) => {
142
- const type = getOptionType(option);
143
-
144
- if (type === "section") {
145
- return `section:${index}:${option.label || ""}`;
146
- }
147
-
148
- if (type === "separator") {
149
- return `separator:${index}`;
150
- }
151
-
152
- return `item:${index}:${option.label || ""}:${stringifyKeyPart(option.value)}`;
153
- }).join("|");
154
- };
155
-
156
- const buildPopoverKey = (options = []) => {
157
- const signature = buildPopoverSignature(options);
158
- let hash = 0;
159
-
160
- for (let index = 0; index < signature.length; index += 1) {
161
- hash = ((hash << 5) - hash + signature.charCodeAt(index)) >>> 0;
162
- }
163
-
164
- return `tagSelectPopover${hash}`;
165
- };
166
-
167
116
  const normalizeOption = ({
168
117
  option = {},
169
118
  index,
@@ -227,7 +176,8 @@ export const selectViewData = ({ state, props }) => {
227
176
  const isDisabled = !!props.disabled;
228
177
  const currentValues = getCurrentValues({ state, props });
229
178
  const draftValues = getDraftValues({ state, props });
230
- const options = Array.isArray(props.options) ? props.options : [];
179
+ const shouldResolveOptions = state.isOpen || currentValues.length > 0;
180
+ const options = shouldResolveOptions && Array.isArray(props.options) ? props.options : [];
231
181
  const hasIconColumn = options.some((option) => isSelectableOption(option) && hasOwnProp(option, "icon"));
232
182
  const normalizedOptions = options.map((option, index) =>
233
183
  normalizeOption({
@@ -254,6 +204,19 @@ export const selectViewData = ({ state, props }) => {
254
204
 
255
205
  const hasSelectableOptions = normalizedOptions.some((option) => option.isItem);
256
206
  const hasDraftChanges = !sameValueArray(currentValues, draftValues);
207
+ const triggerTags = selectedTags.length > 0
208
+ ? selectedTags.map((tag) => ({
209
+ ...tag,
210
+ tagStyle: buildTagStyle({ isSelected: true }),
211
+ }))
212
+ : [{
213
+ value: undefined,
214
+ selectionIndex: "",
215
+ label: props.placeholder || "Add tag",
216
+ icon: "",
217
+ testId: "",
218
+ tagStyle: buildTagStyle({ isAddChip: true }),
219
+ }];
257
220
 
258
221
  return {
259
222
  containerAttrString,
@@ -262,18 +225,14 @@ export const selectViewData = ({ state, props }) => {
262
225
  position: state.position,
263
226
  options: normalizedOptions,
264
227
  hasSelectableOptions,
265
- popoverKey: buildPopoverKey(options),
266
228
  placeholder: props.placeholder || "Add tag",
267
- selectedTags,
268
- hasSelectedTags: selectedTags.length > 0,
269
- triggerTagStyle: buildTagStyle({ isSelected: true }),
270
- placeholderTagStyle: buildTagStyle({ isAddChip: true }),
229
+ triggerTags,
271
230
  triggerCursor: isDisabled ? "not-allowed" : "pointer",
272
231
  triggerTabIndex: isDisabled ? -1 : 0,
273
232
  showAddOption: true,
274
233
  addOptionLabel: props.addOption?.label || "Add tag",
275
234
  hasDraftChanges,
276
- submitDisabled: isDisabled || !hasDraftChanges,
235
+ submitDisabled: isDisabled,
277
236
  submitLabel: "Save",
278
237
  };
279
238
  };
@@ -305,6 +264,10 @@ export const closeOptionsPopover = ({ state }) => {
305
264
  state.draftSelectedValues = [];
306
265
  };
307
266
 
267
+ export const updateDraftSelectedValues = ({ state }, payload = {}) => {
268
+ state.draftSelectedValues = normalizeSelectedValues(payload.values);
269
+ };
270
+
308
271
  export const updateSelectedValues = ({ state }, payload = {}) => {
309
272
  const values = normalizeSelectedValues(payload.values);
310
273
  state.selectedValues = values;
@@ -31,14 +31,11 @@ styles:
31
31
  template:
32
32
  - 'rtgl-view#trigger d=h av=c g=sm cur=${triggerCursor} ${containerAttrString} data-testid="tag-select-trigger" role="button" tabindex=${triggerTabIndex} aria-disabled=${isDisabled} style="min-height: 24px;"':
33
33
  - rtgl-view d=h av=c wrap g=sm w=1fg:
34
- - $if !hasSelectedTags:
35
- - 'rtgl-tag v=mu style="${placeholderTagStyle}"':
36
- ${placeholder}
37
- - $for tag, i in selectedTags:
38
- - 'rtgl-tag#tag${tag.selectionIndex} data-selection-index=${tag.selectionIndex} pre=${tag.icon} data-testid=${tag.testId} v=mu style="${triggerTagStyle}"':
34
+ - $for tag, i in triggerTags:
35
+ - 'rtgl-tag data-selection-index=${tag.selectionIndex} pre=${tag.icon} data-testid=${tag.testId} v=mu style="${tag.tagStyle}"':
39
36
  ${tag.label}
40
- - 'div#${popoverKey} style="display: contents;"':
41
- - rtgl-popover#popover ?open=${isOpen} x=${position.x} y=${position.y} place=bs content-w=${position.w} content-g=sm content-sv=true content-pv=sm:
37
+ - $if isOpen:
38
+ - rtgl-popover#popover ?open=${isOpen} x=${position.x} y=${position.y} place=bs content-w=${position.w} content-g=sm content-sv=true content-ph=md content-pv=md:
42
39
  - $if !hasSelectableOptions:
43
40
  - rtgl-text s=sm c=mu-fg: No tags available
44
41
  - rtgl-view d=h wrap g=sm w=f:
@@ -136,6 +136,7 @@ class RettangoliPopoverElement extends HTMLElement {
136
136
  "content-wh",
137
137
  "content-g",
138
138
  "content-sv",
139
+ "content-ph",
139
140
  "content-pv",
140
141
  "content-bgc",
141
142
  "content-style",
@@ -232,7 +233,7 @@ class RettangoliPopoverElement extends HTMLElement {
232
233
  }
233
234
 
234
235
  wrapper.setAttribute("bgc", this.getAttribute("content-bgc") || "su");
235
- wrapper.setAttribute("ph", "sm");
236
+ wrapper.setAttribute("ph", this.getAttribute("content-ph") || "sm");
236
237
  wrapper.setAttribute("pv", this.getAttribute("content-pv") || "sm");
237
238
 
238
239
  const contentStyle = this.getAttribute("content-style");