@rettangoli/ui 1.6.1 → 1.7.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/dist/rettangoli-iife-ui.min.js +67 -67
- package/package.json +1 -1
- package/src/components/form/form.handlers.js +8 -0
- package/src/components/form/form.methods.js +9 -4
- package/src/components/form/form.store.js +5 -1
- package/src/components/form/form.view.yaml +2 -0
- package/src/components/tagSelect/tagSelect.handlers.js +192 -0
- package/src/components/tagSelect/tagSelect.methods.js +32 -0
- package/src/components/tagSelect/tagSelect.schema.yaml +51 -0
- package/src/components/tagSelect/tagSelect.store.js +335 -0
- package/src/components/tagSelect/tagSelect.view.yaml +61 -0
package/package.json
CHANGED
|
@@ -71,6 +71,14 @@ const updateFieldAttributes = ({
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
if (field.type === "tag-select" && ref.store?.updateSelectedValues) {
|
|
75
|
+
const value = get(formValues, field.name);
|
|
76
|
+
ref.store.updateSelectedValues({ values: Array.isArray(value) ? value : [] });
|
|
77
|
+
if (typeof ref.render === "function") {
|
|
78
|
+
ref.render();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
if (field.type === "checkbox") {
|
|
75
83
|
const value = get(formValues, field.name);
|
|
76
84
|
if (value) {
|
|
@@ -60,8 +60,13 @@ const syncFieldValueAttribute = ({ ref, fieldType, value, forceRefresh = false }
|
|
|
60
60
|
|
|
61
61
|
const syncChoiceFieldState = ({ ref, value }) => {
|
|
62
62
|
if (!ref) return;
|
|
63
|
-
if (
|
|
64
|
-
|
|
63
|
+
if (ref?.store?.updateSelectedValues) {
|
|
64
|
+
ref.store.updateSelectedValues({ values: Array.isArray(value) ? value : [] });
|
|
65
|
+
} else if (ref?.store?.updateSelectedValue) {
|
|
66
|
+
ref.store.updateSelectedValue({ value });
|
|
67
|
+
} else {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
65
70
|
if (typeof ref.render === "function") {
|
|
66
71
|
ref.render();
|
|
67
72
|
}
|
|
@@ -93,7 +98,7 @@ const syncChoiceRefsFromValues = ({ root, values = {} }) => {
|
|
|
93
98
|
if (!root || typeof root.querySelectorAll !== "function") return;
|
|
94
99
|
|
|
95
100
|
const choiceRefs = root.querySelectorAll(
|
|
96
|
-
"rtgl-select[data-field-name], rtgl-segmented-control[data-field-name]",
|
|
101
|
+
"rtgl-select[data-field-name], rtgl-tag-select[data-field-name], rtgl-segmented-control[data-field-name]",
|
|
97
102
|
);
|
|
98
103
|
choiceRefs.forEach((ref) => {
|
|
99
104
|
const fieldName = ref.dataset?.fieldName;
|
|
@@ -142,7 +147,7 @@ export const setValues = function (payload = {}) {
|
|
|
142
147
|
forceRefresh: true,
|
|
143
148
|
});
|
|
144
149
|
|
|
145
|
-
if (
|
|
150
|
+
if (["select", "tag-select", "segmented-control"].includes(field.type)) {
|
|
146
151
|
syncChoiceFieldState({ ref, value });
|
|
147
152
|
}
|
|
148
153
|
|
|
@@ -284,6 +284,7 @@ export const validateField = (field, value) => {
|
|
|
284
284
|
value === undefined ||
|
|
285
285
|
value === null ||
|
|
286
286
|
value === "" ||
|
|
287
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
287
288
|
(typeof value === "boolean" && value === false);
|
|
288
289
|
// For numbers, 0 is a valid value
|
|
289
290
|
const isEmptyNumber = field.type === "input-number" && value === null;
|
|
@@ -404,6 +405,8 @@ export const getDefaultValue = (field) => {
|
|
|
404
405
|
case "select":
|
|
405
406
|
case "segmented-control":
|
|
406
407
|
return null;
|
|
408
|
+
case "tag-select":
|
|
409
|
+
return [];
|
|
407
410
|
case "checkbox":
|
|
408
411
|
return false;
|
|
409
412
|
case "color-picker":
|
|
@@ -507,9 +510,10 @@ export const selectViewData = ({ state, props }) => {
|
|
|
507
510
|
field._inputType = field.inputType || "text";
|
|
508
511
|
}
|
|
509
512
|
|
|
510
|
-
if (field.type === "select" || field.type === "segmented-control") {
|
|
513
|
+
if (field.type === "select" || field.type === "tag-select" || field.type === "segmented-control") {
|
|
511
514
|
const val = get(state.formValues, field.name);
|
|
512
515
|
field._selectedValue = val !== undefined ? val : null;
|
|
516
|
+
field._selectedValues = Array.isArray(val) ? val : [];
|
|
513
517
|
field.placeholder = field.placeholder || "";
|
|
514
518
|
// clearable defaults to true; noClear is the inverse
|
|
515
519
|
field.noClear = field.clearable === false;
|
|
@@ -68,6 +68,8 @@ template:
|
|
|
68
68
|
- rtgl-textarea#field${field._idx} data-field-name=${field.name} w=f rows=${field.rows} placeholder=${field.placeholder} ?disabled=${field._disabled}: null
|
|
69
69
|
- $if field.type == "select":
|
|
70
70
|
- rtgl-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} ?no-clear=${field.noClear} :selectedValue=${field._selectedValue} :placeholder=${field.placeholder} ?disabled=${field._disabled}: null
|
|
71
|
+
- $if field.type == "tag-select":
|
|
72
|
+
- rtgl-tag-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} :selectedValues=${field._selectedValues} :placeholder=${field.placeholder} :addOption=${field.addOption} ?disabled=${field._disabled}: null
|
|
71
73
|
- $if field.type == "segmented-control":
|
|
72
74
|
- rtgl-segmented-control#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} ?no-clear=${field.noClear} :selectedValue=${field._selectedValue} :placeholder=${field.placeholder} ?disabled=${field._disabled}: null
|
|
73
75
|
- $if field.type == "color-picker":
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const getOptionType = (option = {}) => {
|
|
2
|
+
if (option.type === "section") {
|
|
3
|
+
return "section";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (option.type === "separator") {
|
|
7
|
+
return "separator";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return "item";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const normalizeSelectedValues = (selectedValues) => {
|
|
14
|
+
if (!Array.isArray(selectedValues)) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return [...selectedValues];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const resolveCurrentValues = ({ store, props }) => {
|
|
22
|
+
if (store.selectHasSelectedValues()) {
|
|
23
|
+
return store.selectSelectedValues();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return normalizeSelectedValues(props.selectedValues);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const emitValueChange = ({
|
|
30
|
+
dispatchEvent,
|
|
31
|
+
value,
|
|
32
|
+
operation,
|
|
33
|
+
changedValue,
|
|
34
|
+
label,
|
|
35
|
+
index,
|
|
36
|
+
item,
|
|
37
|
+
}) => {
|
|
38
|
+
dispatchEvent(
|
|
39
|
+
new CustomEvent("value-change", {
|
|
40
|
+
detail: {
|
|
41
|
+
value,
|
|
42
|
+
operation,
|
|
43
|
+
changedValue,
|
|
44
|
+
label,
|
|
45
|
+
index,
|
|
46
|
+
item,
|
|
47
|
+
},
|
|
48
|
+
bubbles: true,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const handleBeforeMount = (deps) => {
|
|
54
|
+
const { store, props, render } = deps;
|
|
55
|
+
|
|
56
|
+
if (props.selectedValues !== undefined) {
|
|
57
|
+
store.updateSelectedValues({ values: props.selectedValues });
|
|
58
|
+
render();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const handleOnUpdate = (deps, payload) => {
|
|
63
|
+
const { oldProps, newProps } = payload;
|
|
64
|
+
const { store, render } = deps;
|
|
65
|
+
let shouldRender = false;
|
|
66
|
+
|
|
67
|
+
if (!!newProps?.disabled && !oldProps?.disabled) {
|
|
68
|
+
store.closeOptionsPopover({});
|
|
69
|
+
shouldRender = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (oldProps.selectedValues !== newProps.selectedValues) {
|
|
73
|
+
store.updateSelectedValues({ values: newProps.selectedValues, syncDraft: true });
|
|
74
|
+
shouldRender = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (oldProps.options !== newProps.options) {
|
|
78
|
+
shouldRender = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (shouldRender) {
|
|
82
|
+
render();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const handleTriggerClick = (deps, payload) => {
|
|
87
|
+
const { store, render, refs, props } = deps;
|
|
88
|
+
if (props.disabled) return;
|
|
89
|
+
|
|
90
|
+
const event = payload._event;
|
|
91
|
+
event.stopPropagation();
|
|
92
|
+
|
|
93
|
+
const trigger = refs.trigger;
|
|
94
|
+
const rect = trigger.getBoundingClientRect();
|
|
95
|
+
const currentValues = resolveCurrentValues({ store, props });
|
|
96
|
+
|
|
97
|
+
store.openOptionsPopover({
|
|
98
|
+
position: {
|
|
99
|
+
x: Math.round(rect.left),
|
|
100
|
+
y: Math.round(rect.bottom + 12),
|
|
101
|
+
w: Math.max(Math.round(rect.width), 240),
|
|
102
|
+
},
|
|
103
|
+
values: currentValues,
|
|
104
|
+
});
|
|
105
|
+
render();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const handleTriggerKeyDown = (deps, payload) => {
|
|
109
|
+
const event = payload._event;
|
|
110
|
+
if (event.key !== "Enter" && event.key !== " ") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
handleTriggerClick(deps, payload);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const handlePopoverClose = (deps) => {
|
|
119
|
+
const { store, render } = deps;
|
|
120
|
+
store.closeOptionsPopover({});
|
|
121
|
+
render();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const handleOptionClick = (deps, payload) => {
|
|
125
|
+
const { render, props, store } = deps;
|
|
126
|
+
if (props.disabled) return;
|
|
127
|
+
|
|
128
|
+
const event = payload._event;
|
|
129
|
+
event.stopPropagation();
|
|
130
|
+
|
|
131
|
+
const id = event.currentTarget.id.slice("option".length);
|
|
132
|
+
const index = Number(id);
|
|
133
|
+
const option = props.options[index];
|
|
134
|
+
|
|
135
|
+
if (!option || getOptionType(option) !== "item") {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
store.toggleDraftSelectedValue({ value: option.value });
|
|
140
|
+
render();
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const handleOptionKeyDown = (deps, payload) => {
|
|
144
|
+
const event = payload._event;
|
|
145
|
+
if (event.key !== "Enter" && event.key !== " ") {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
handleOptionClick(deps, payload);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const handleSubmitClick = (deps, payload) => {
|
|
154
|
+
const { store, props, render, dispatchEvent } = deps;
|
|
155
|
+
if (props.disabled) return;
|
|
156
|
+
|
|
157
|
+
const event = payload._event;
|
|
158
|
+
event.stopPropagation();
|
|
159
|
+
|
|
160
|
+
const nextValues = store.selectDraftSelectedValues();
|
|
161
|
+
|
|
162
|
+
store.commitDraftSelectedValues({});
|
|
163
|
+
|
|
164
|
+
if (props.onChange && typeof props.onChange === "function") {
|
|
165
|
+
props.onChange(nextValues);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
emitValueChange({
|
|
169
|
+
dispatchEvent,
|
|
170
|
+
value: nextValues,
|
|
171
|
+
operation: "set",
|
|
172
|
+
changedValue: undefined,
|
|
173
|
+
label: undefined,
|
|
174
|
+
index: null,
|
|
175
|
+
item: undefined,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
render();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const handleAddOptionClick = (deps, payload) => {
|
|
182
|
+
const { props, dispatchEvent } = deps;
|
|
183
|
+
if (props.disabled) return;
|
|
184
|
+
|
|
185
|
+
const event = payload._event;
|
|
186
|
+
event.stopPropagation();
|
|
187
|
+
|
|
188
|
+
dispatchEvent(new CustomEvent("add-option-click", {
|
|
189
|
+
bubbles: true,
|
|
190
|
+
composed: true,
|
|
191
|
+
}));
|
|
192
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const resolvePopoverPosition = (trigger) => {
|
|
2
|
+
if (!trigger || typeof trigger.getBoundingClientRect !== "function") {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const rect = trigger.getBoundingClientRect();
|
|
7
|
+
return {
|
|
8
|
+
x: Math.round(rect.left),
|
|
9
|
+
y: Math.round(rect.bottom + 12),
|
|
10
|
+
w: Math.max(Math.round(rect.width), 240),
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const refreshPopover = function () {
|
|
15
|
+
const state = this.store?.getState?.();
|
|
16
|
+
|
|
17
|
+
if (state?.isOpen) {
|
|
18
|
+
const position = resolvePopoverPosition(this.refs?.trigger);
|
|
19
|
+
const draftValues = this.store?.selectDraftSelectedValues?.() || [];
|
|
20
|
+
|
|
21
|
+
if (position) {
|
|
22
|
+
this.store.openOptionsPopover({
|
|
23
|
+
position,
|
|
24
|
+
values: draftValues,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof this.render === "function") {
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
componentName: rtgl-tag-select
|
|
2
|
+
propsSchema:
|
|
3
|
+
type: object
|
|
4
|
+
properties:
|
|
5
|
+
placeholder:
|
|
6
|
+
type: string
|
|
7
|
+
options:
|
|
8
|
+
type: array
|
|
9
|
+
items:
|
|
10
|
+
type: object
|
|
11
|
+
properties:
|
|
12
|
+
type:
|
|
13
|
+
type: string
|
|
14
|
+
enum:
|
|
15
|
+
- section
|
|
16
|
+
- item
|
|
17
|
+
- separator
|
|
18
|
+
label:
|
|
19
|
+
type: string
|
|
20
|
+
value:
|
|
21
|
+
type: any
|
|
22
|
+
icon:
|
|
23
|
+
type: string
|
|
24
|
+
shortcut:
|
|
25
|
+
type: string
|
|
26
|
+
suffixText:
|
|
27
|
+
type: string
|
|
28
|
+
testId:
|
|
29
|
+
type: string
|
|
30
|
+
selectedValues:
|
|
31
|
+
type: array
|
|
32
|
+
items:
|
|
33
|
+
type: any
|
|
34
|
+
onChange:
|
|
35
|
+
type: function
|
|
36
|
+
addOption:
|
|
37
|
+
type: object
|
|
38
|
+
properties:
|
|
39
|
+
label:
|
|
40
|
+
type: string
|
|
41
|
+
disabled:
|
|
42
|
+
type: boolean
|
|
43
|
+
w:
|
|
44
|
+
type: string
|
|
45
|
+
events:
|
|
46
|
+
value-change: {}
|
|
47
|
+
add-option-click: {}
|
|
48
|
+
methods:
|
|
49
|
+
type: object
|
|
50
|
+
properties:
|
|
51
|
+
refreshPopover: {}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { deepEqual } from "../../common.js";
|
|
2
|
+
|
|
3
|
+
const blacklistedProps = [
|
|
4
|
+
"id",
|
|
5
|
+
"class",
|
|
6
|
+
"style",
|
|
7
|
+
"slot",
|
|
8
|
+
"placeholder",
|
|
9
|
+
"options",
|
|
10
|
+
"selectedValues",
|
|
11
|
+
"onChange",
|
|
12
|
+
"addOption",
|
|
13
|
+
"disabled",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const stringifyProps = (props = {}) => {
|
|
17
|
+
return Object.entries(props)
|
|
18
|
+
.filter(([key]) => !blacklistedProps.includes(key))
|
|
19
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
20
|
+
.join(" ");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const hasOwnProp = (object, key) => Object.prototype.hasOwnProperty.call(object || {}, key);
|
|
24
|
+
|
|
25
|
+
const getOptionType = (option = {}) => {
|
|
26
|
+
if (option.type === "section") {
|
|
27
|
+
return "section";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (option.type === "separator") {
|
|
31
|
+
return "separator";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return "item";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const isSelectableOption = (option = {}) => getOptionType(option) === "item";
|
|
38
|
+
|
|
39
|
+
const getOptionIcon = (option = {}) => {
|
|
40
|
+
return typeof option.icon === "string" && option.icon.length > 0 ? option.icon : "";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getOptionSuffixText = (option = {}) => {
|
|
44
|
+
if (typeof option.shortcut === "string" && option.shortcut.length > 0) {
|
|
45
|
+
return option.shortcut;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof option.suffixText === "string" && option.suffixText.length > 0) {
|
|
49
|
+
return option.suffixText;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return "";
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const normalizeSelectedValues = (selectedValues) => {
|
|
56
|
+
if (!Array.isArray(selectedValues)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return [...selectedValues];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const sameValueArray = (left = [], right = []) => {
|
|
64
|
+
if (left.length !== right.length) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return left.every((value, index) => deepEqual(value, right[index]));
|
|
69
|
+
};
|
|
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
|
+
const isSelectedValue = (selectedValues = [], candidate) => {
|
|
92
|
+
return selectedValues.some((value) => deepEqual(value, candidate));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const getCurrentValues = ({ state, props }) => {
|
|
96
|
+
if (state.hasSelectedValues) {
|
|
97
|
+
return normalizeSelectedValues(state.selectedValues);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return normalizeSelectedValues(props.selectedValues);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const getDraftValues = ({ state, props }) => {
|
|
104
|
+
if (state.isOpen) {
|
|
105
|
+
return normalizeSelectedValues(state.draftSelectedValues);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return getCurrentValues({ state, props });
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const stringifyFallbackLabel = (value) => {
|
|
112
|
+
if (value === undefined || value === null) {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return String(value);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const findMatchingOption = (options = [], value) => {
|
|
120
|
+
return options.find((option) => isSelectableOption(option) && deepEqual(option.value, value));
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const buildTagStyle = ({ isSelected = true, isAddChip = false } = {}) => {
|
|
124
|
+
const baseStyle = [
|
|
125
|
+
"--tag-border-radius: var(--border-radius-md)",
|
|
126
|
+
"--muted-foreground: var(--foreground)",
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
if (isAddChip || !isSelected) {
|
|
130
|
+
baseStyle.push("--muted: color-mix(in srgb, var(--muted) 82%, var(--background) 18%)");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return `${baseStyle.join("; ")};`;
|
|
134
|
+
};
|
|
135
|
+
|
|
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
|
+
const normalizeOption = ({
|
|
168
|
+
option = {},
|
|
169
|
+
index,
|
|
170
|
+
selectedValues = [],
|
|
171
|
+
hasIconColumn = false,
|
|
172
|
+
}) => {
|
|
173
|
+
const type = getOptionType(option);
|
|
174
|
+
const isSection = type === "section";
|
|
175
|
+
const isSeparator = type === "separator";
|
|
176
|
+
const isItem = type === "item";
|
|
177
|
+
|
|
178
|
+
if (isSection || isSeparator) {
|
|
179
|
+
return {
|
|
180
|
+
...option,
|
|
181
|
+
index,
|
|
182
|
+
type,
|
|
183
|
+
isSection,
|
|
184
|
+
isSeparator,
|
|
185
|
+
isItem,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const isSelected = isSelectedValue(selectedValues, option.value);
|
|
190
|
+
const icon = getOptionIcon(option);
|
|
191
|
+
const baseSuffixText = getOptionSuffixText(option);
|
|
192
|
+
const suffixText = isSelected ? (baseSuffixText || "Added") : baseSuffixText;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...option,
|
|
196
|
+
index,
|
|
197
|
+
type,
|
|
198
|
+
isSection,
|
|
199
|
+
isSeparator,
|
|
200
|
+
isItem,
|
|
201
|
+
isSelected,
|
|
202
|
+
hasIconSlot: hasIconColumn,
|
|
203
|
+
icon,
|
|
204
|
+
hasIcon: icon.length > 0,
|
|
205
|
+
cursor: "pointer",
|
|
206
|
+
tagStyle: buildTagStyle({ isSelected }),
|
|
207
|
+
suffixText,
|
|
208
|
+
hasSuffixText: suffixText.length > 0,
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const createInitialState = () =>
|
|
213
|
+
Object.freeze({
|
|
214
|
+
isOpen: false,
|
|
215
|
+
position: {
|
|
216
|
+
x: 0,
|
|
217
|
+
y: 0,
|
|
218
|
+
w: 240,
|
|
219
|
+
},
|
|
220
|
+
selectedValues: [],
|
|
221
|
+
draftSelectedValues: [],
|
|
222
|
+
hasSelectedValues: false,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
export const selectViewData = ({ state, props }) => {
|
|
226
|
+
const containerAttrString = stringifyProps(props);
|
|
227
|
+
const isDisabled = !!props.disabled;
|
|
228
|
+
const currentValues = getCurrentValues({ state, props });
|
|
229
|
+
const draftValues = getDraftValues({ state, props });
|
|
230
|
+
const options = Array.isArray(props.options) ? props.options : [];
|
|
231
|
+
const hasIconColumn = options.some((option) => isSelectableOption(option) && hasOwnProp(option, "icon"));
|
|
232
|
+
const normalizedOptions = options.map((option, index) =>
|
|
233
|
+
normalizeOption({
|
|
234
|
+
option,
|
|
235
|
+
index,
|
|
236
|
+
selectedValues: draftValues,
|
|
237
|
+
hasIconColumn,
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const selectedTags = currentValues.map((value, selectionIndex) => {
|
|
242
|
+
const matchedOption = findMatchingOption(options, value);
|
|
243
|
+
const icon = matchedOption ? getOptionIcon(matchedOption) : "";
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
value,
|
|
247
|
+
selectionIndex,
|
|
248
|
+
label: matchedOption?.label || stringifyFallbackLabel(value),
|
|
249
|
+
icon,
|
|
250
|
+
hasIcon: icon.length > 0,
|
|
251
|
+
testId: `tag-select-tag-${selectionIndex}`,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const hasSelectableOptions = normalizedOptions.some((option) => option.isItem);
|
|
256
|
+
const hasDraftChanges = !sameValueArray(currentValues, draftValues);
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
containerAttrString,
|
|
260
|
+
isDisabled,
|
|
261
|
+
isOpen: state.isOpen,
|
|
262
|
+
position: state.position,
|
|
263
|
+
options: normalizedOptions,
|
|
264
|
+
hasSelectableOptions,
|
|
265
|
+
popoverKey: buildPopoverKey(options),
|
|
266
|
+
placeholder: props.placeholder || "Add tag",
|
|
267
|
+
selectedTags,
|
|
268
|
+
hasSelectedTags: selectedTags.length > 0,
|
|
269
|
+
triggerTagStyle: buildTagStyle({ isSelected: true }),
|
|
270
|
+
placeholderTagStyle: buildTagStyle({ isAddChip: true }),
|
|
271
|
+
triggerCursor: isDisabled ? "not-allowed" : "pointer",
|
|
272
|
+
triggerTabIndex: isDisabled ? -1 : 0,
|
|
273
|
+
showAddOption: true,
|
|
274
|
+
addOptionLabel: props.addOption?.label || "Add tag",
|
|
275
|
+
hasDraftChanges,
|
|
276
|
+
submitDisabled: isDisabled || !hasDraftChanges,
|
|
277
|
+
submitLabel: "Save",
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const selectSelectedValues = ({ state }) => {
|
|
282
|
+
return normalizeSelectedValues(state.selectedValues);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const selectHasSelectedValues = ({ state }) => {
|
|
286
|
+
return !!state.hasSelectedValues;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export const selectDraftSelectedValues = ({ state }) => {
|
|
290
|
+
return normalizeSelectedValues(state.draftSelectedValues);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export const openOptionsPopover = ({ state }, payload = {}) => {
|
|
294
|
+
const { position, values } = payload;
|
|
295
|
+
state.position = {
|
|
296
|
+
...state.position,
|
|
297
|
+
...(position || {}),
|
|
298
|
+
};
|
|
299
|
+
state.draftSelectedValues = normalizeSelectedValues(values);
|
|
300
|
+
state.isOpen = true;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export const closeOptionsPopover = ({ state }) => {
|
|
304
|
+
state.isOpen = false;
|
|
305
|
+
state.draftSelectedValues = [];
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export const updateSelectedValues = ({ state }, payload = {}) => {
|
|
309
|
+
const values = normalizeSelectedValues(payload.values);
|
|
310
|
+
state.selectedValues = values;
|
|
311
|
+
state.hasSelectedValues = true;
|
|
312
|
+
if (payload.syncDraft || !state.isOpen) {
|
|
313
|
+
state.draftSelectedValues = [...values];
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export const toggleDraftSelectedValue = ({ state }, payload = {}) => {
|
|
318
|
+
const draftValues = normalizeSelectedValues(state.draftSelectedValues);
|
|
319
|
+
const value = payload.value;
|
|
320
|
+
const existingIndex = draftValues.findIndex((currentValue) => deepEqual(currentValue, value));
|
|
321
|
+
|
|
322
|
+
if (existingIndex >= 0) {
|
|
323
|
+
state.draftSelectedValues = draftValues.filter((_, index) => index !== existingIndex);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
state.draftSelectedValues = [...draftValues, value];
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export const commitDraftSelectedValues = ({ state }) => {
|
|
331
|
+
state.selectedValues = normalizeSelectedValues(state.draftSelectedValues);
|
|
332
|
+
state.hasSelectedValues = true;
|
|
333
|
+
state.isOpen = false;
|
|
334
|
+
state.draftSelectedValues = [];
|
|
335
|
+
};
|