jsswift 2.0.1 → 2.1.1
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/AI.md +30 -3
- package/README.md +2 -0
- package/dist/build/build-configured-runtime.d.mts +29 -0
- package/dist/build/build-configured-runtime.mjs +92 -0
- package/dist/build/runtime-layout.mjs +13 -0
- package/dist/build/select-ui.mjs +167 -0
- package/dist/build/vite-jss.d.mts +2 -0
- package/dist/build/vite-jss.mjs +73 -0
- package/dist/cms.js +3522 -0
- package/dist/css/min-ui.css +1 -1
- package/dist/css/ui-components.css +13 -0
- package/dist/css/ui-light.css +13 -0
- package/dist/css/ui-light.min.css +1 -1
- package/dist/css/ui.css +13 -0
- package/dist/css/ui.min.css +1 -1
- package/dist/documentions/AI.md +30 -3
- package/dist/documentions/README.md +1 -0
- package/dist/documentions/components/table.md +3 -0
- package/dist/documentions/configured-runtime.md +82 -0
- package/dist/documentions/examples/crud.md +9 -5
- package/dist/documentions/knowledge-map.json +20 -1
- package/dist/jsswift.js +2 -2
- package/dist/jsswift.min.js +1 -1
- package/dist/min-jsswift.js +1 -1
- package/dist/ui-src/00-bootstrap.js +330 -0
- package/dist/ui-src/01-foundation-helpers.js +809 -0
- package/dist/ui-src/10-primitives-layout.js +5091 -0
- package/dist/ui-src/20-display-content.js +780 -0
- package/dist/ui-src/21-list-content.js +246 -0
- package/dist/ui-src/22-banner-content.js +376 -0
- package/dist/ui-src/23-tooltip.js +276 -0
- package/dist/ui-src/31-form-advanced.js +2784 -0
- package/dist/ui-src/40-navigation.js +1262 -0
- package/dist/ui-src/41-tab-panel.js +896 -0
- package/dist/ui-src/50-feedback.js +999 -0
- package/dist/ui-src/51-feedback-service.js +166 -0
- package/dist/ui-src/60-shell-app.js +1223 -0
- package/dist/ui-src/61-form-service.js +368 -0
- package/dist/ui-src/70-dialog.js +333 -0
- package/dist/ui-src/71-menu-overlays.js +1217 -0
- package/dist/ui-src/80-data.js +437 -0
- package/dist/ui-src/modules.json +212 -0
- package/dist/ui.js +12565 -0
- package/package.json +26 -2
|
@@ -0,0 +1,2784 @@
|
|
|
1
|
+
const buildChoiceControl = (type, args, options = {}) => {
|
|
2
|
+
const { props, children } = JSswift.uiNormalizeArgs(args);
|
|
3
|
+
const slots = props.slots || {};
|
|
4
|
+
const isRadio = type === "radio";
|
|
5
|
+
const isToggle = options.appearance === "toggle";
|
|
6
|
+
const supportsStandby = !isRadio;
|
|
7
|
+
const id = props.id || (`jss-${type}-` + Math.random().toString(36).slice(2));
|
|
8
|
+
const model = resolveModel(
|
|
9
|
+
props.model,
|
|
10
|
+
options.modelName || (isRadio ? "UI.Radio:model" : "UI.Checkbox:model")
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const inputProps = JSswift.omit(props, [
|
|
14
|
+
"model", "label", "checked", "class", "style", "dense", "onChange", "onInput", "slots",
|
|
15
|
+
"icon", "iconOn", "iconOff", "iconStandby", "checkedIcon", "uncheckedIcon", "standbyIcon",
|
|
16
|
+
"indeterminateIcon", "inputClass", "iconSize", "color", "size", "outline", "behavior", "mode",
|
|
17
|
+
"flat", "glossy", "glow", "glass", "gradient", "textGradient", "lightShadow", "shadow",
|
|
18
|
+
"rounded", "radius", "textColor", "clickable", "border",
|
|
19
|
+
"shortcode", "shortcut", "hotkey", "showShortcode", "showShortcut"
|
|
20
|
+
]);
|
|
21
|
+
inputProps.type = type;
|
|
22
|
+
inputProps.id = id;
|
|
23
|
+
if (isRadio && props.name != null) inputProps.name = props.name;
|
|
24
|
+
inputProps.class = uiClass([`jss-${type}`, "jss-choice-input", props.inputClass]);
|
|
25
|
+
const input = _.input(inputProps);
|
|
26
|
+
|
|
27
|
+
const labelNodes = renderSlotToArray(slots, "label", {}, props.label);
|
|
28
|
+
const labelContent = labelNodes.length ? labelNodes : renderSlotToArray(slots, "default", {}, children);
|
|
29
|
+
const shortcodeHint = uiCreateShortcodeHint(props, { className: "jss-shortcode jss-choice-shortcode" });
|
|
30
|
+
const finalLabelContent = shortcodeHint ? [...labelContent, shortcodeHint] : labelContent;
|
|
31
|
+
|
|
32
|
+
const wrapProps = JSswift.omit(props, [
|
|
33
|
+
"model", "label", "checked", "onChange", "onInput", "value", "name", "id", "type", "dense",
|
|
34
|
+
"inputClass", "slots", "icon", "iconOn", "iconOff", "iconStandby", "checkedIcon",
|
|
35
|
+
"uncheckedIcon", "standbyIcon", "indeterminateIcon", "iconSize", "color", "size", "behavior",
|
|
36
|
+
"mode",
|
|
37
|
+
"outline", "flat", "glossy", "glow", "glass", "gradient", "textGradient", "lightShadow",
|
|
38
|
+
"shadow", "rounded", "radius", "textColor", "clickable", "border",
|
|
39
|
+
"shortcode", "shortcut", "hotkey", "showShortcode", "showShortcut"
|
|
40
|
+
]);
|
|
41
|
+
wrapProps.class = uiClass([
|
|
42
|
+
"jss-clear-set",
|
|
43
|
+
"jss-singularity-check",
|
|
44
|
+
"jss-choice-wrap",
|
|
45
|
+
`jss-${type}-wrap`,
|
|
46
|
+
uiWhen(isToggle, "jss-toggle-wrap"),
|
|
47
|
+
uiWhen(isToggle && isRadio, "jss-toggle-radio-wrap"),
|
|
48
|
+
uiWhen(isToggle && !isRadio, "jss-toggle-checkbox-wrap"),
|
|
49
|
+
uiWhen(props.dense, "dense"),
|
|
50
|
+
props.class
|
|
51
|
+
]);
|
|
52
|
+
wrapProps.style = { ...(props.style || {}) };
|
|
53
|
+
|
|
54
|
+
const sizeValue = uiUnwrap(props.size);
|
|
55
|
+
if (sizeValue != null && !(typeof sizeValue === "string" && JSswift.uiSizes?.includes(sizeValue))) {
|
|
56
|
+
wrapProps.style["--jss-choice-size"] = toCssSize(sizeValue);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const marker = _.span({
|
|
60
|
+
class: uiClass([
|
|
61
|
+
"jss-choice-mark",
|
|
62
|
+
isRadio ? "jss-choice-radio-mark" : "jss-choice-checkbox-mark",
|
|
63
|
+
uiWhen(isToggle, "jss-toggle-mark")
|
|
64
|
+
])
|
|
65
|
+
});
|
|
66
|
+
const indicatorHost = isToggle ? _.span({ class: "jss-toggle-thumb" }) : marker;
|
|
67
|
+
if (isToggle) marker.appendChild(indicatorHost);
|
|
68
|
+
const labelNode = finalLabelContent.length ? _.span({ class: "jss-choice-label" }, ...finalLabelContent) : null;
|
|
69
|
+
|
|
70
|
+
const wrap = _.label(
|
|
71
|
+
wrapProps,
|
|
72
|
+
input,
|
|
73
|
+
marker,
|
|
74
|
+
labelNode
|
|
75
|
+
);
|
|
76
|
+
setPropertyProps(wrap, props);
|
|
77
|
+
|
|
78
|
+
const iconSize = props.iconSize ? props.iconSize : props.size;
|
|
79
|
+
const defaultCheckedIcon = isToggle ? null : (isRadio ? "radio_button_checked" : "check");
|
|
80
|
+
const defaultUncheckedIcon = isToggle ? null : (isRadio ? "radio_button_unchecked" : null);
|
|
81
|
+
const defaultStandbyIcon = isToggle || isRadio ? null : "indeterminate_check_box";
|
|
82
|
+
const setInputState = (value) => {
|
|
83
|
+
if (isRadio) {
|
|
84
|
+
input.checked = value == props.value;
|
|
85
|
+
return input.checked;
|
|
86
|
+
}
|
|
87
|
+
const normalized = value == null ? null : !!value;
|
|
88
|
+
input.checked = normalized === true;
|
|
89
|
+
input.indeterminate = supportsStandby && normalized == null;
|
|
90
|
+
return normalized;
|
|
91
|
+
};
|
|
92
|
+
const getInputState = () => {
|
|
93
|
+
if (!isRadio && supportsStandby && input.indeterminate) return null;
|
|
94
|
+
return !!input.checked;
|
|
95
|
+
};
|
|
96
|
+
const resolveIconSource = (state) => {
|
|
97
|
+
if (state === true) {
|
|
98
|
+
if (props.checkedIcon != null) return props.checkedIcon;
|
|
99
|
+
if (props.icon != null) return props.icon;
|
|
100
|
+
return defaultCheckedIcon;
|
|
101
|
+
}
|
|
102
|
+
if (state === false) {
|
|
103
|
+
if (props.uncheckedIcon != null) return props.uncheckedIcon;
|
|
104
|
+
return defaultUncheckedIcon;
|
|
105
|
+
}
|
|
106
|
+
if (props.indeterminateIcon != null) return props.indeterminateIcon;
|
|
107
|
+
if (props.standbyIcon != null) return props.standbyIcon;
|
|
108
|
+
if (props.iconStandby != null) return props.iconStandby;
|
|
109
|
+
return defaultStandbyIcon;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const syncIndicator = () => {
|
|
113
|
+
const state = getInputState();
|
|
114
|
+
const checked = state === true;
|
|
115
|
+
while (indicatorHost.firstChild) indicatorHost.removeChild(indicatorHost.firstChild);
|
|
116
|
+
wrap.classList.toggle("is-checked", checked);
|
|
117
|
+
wrap.classList.toggle("is-indeterminate", state == null);
|
|
118
|
+
wrap.classList.toggle("is-disabled", !!input.disabled);
|
|
119
|
+
if (checked && (props.checkedIcon || props.icon)) {
|
|
120
|
+
wrap.classList.toggle("toggle-default", false);
|
|
121
|
+
} else if (checked && !props.checkedIcon) {
|
|
122
|
+
wrap.classList.toggle("toggle-default", true);
|
|
123
|
+
} else if (state == null && !props.standbyIcon) {
|
|
124
|
+
wrap.classList.toggle("toggle-default", true);
|
|
125
|
+
} else if (state == null && props.standbyIcon) {
|
|
126
|
+
wrap.classList.toggle("toggle-default", false);
|
|
127
|
+
} else if (input.disabled === false && !props.uncheckedIcon) {
|
|
128
|
+
wrap.classList.toggle("toggle-default", true);
|
|
129
|
+
} else {
|
|
130
|
+
wrap.classList.toggle("toggle-default", false);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isToggle) {
|
|
134
|
+
if (state == null) {
|
|
135
|
+
indicatorHost.style.transform = "translateX(calc((var(--jss-toggle-width) - var(--jss-toggle-thumb-size) - 1px) / 2))";
|
|
136
|
+
} else {
|
|
137
|
+
indicatorHost.style.removeProperty("transform");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const ctx = { checked, state, indeterminate: state == null, value: props.value, id, type };
|
|
142
|
+
let iconNode = null;
|
|
143
|
+
if (state === true) {
|
|
144
|
+
iconNode = JSswift.ui.renderSlot(slots, "checkedIcon", ctx, null);
|
|
145
|
+
if (iconNode == null) iconNode = JSswift.ui.renderSlot(slots, "iconOn", ctx, null);
|
|
146
|
+
} else if (state === false) {
|
|
147
|
+
iconNode = JSswift.ui.renderSlot(slots, "uncheckedIcon", ctx, null);
|
|
148
|
+
if (iconNode == null) iconNode = JSswift.ui.renderSlot(slots, "iconOff", ctx, null);
|
|
149
|
+
} else {
|
|
150
|
+
iconNode = JSswift.ui.renderSlot(slots, "indeterminateIcon", ctx, null);
|
|
151
|
+
if (iconNode == null) iconNode = JSswift.ui.renderSlot(slots, "standbyIcon", ctx, null);
|
|
152
|
+
if (iconNode == null) iconNode = JSswift.ui.renderSlot(slots, "iconStandby", ctx, null);
|
|
153
|
+
}
|
|
154
|
+
if (iconNode == null) iconNode = JSswift.ui.renderSlot(slots, "icon", ctx, null);
|
|
155
|
+
if (iconNode == null) {
|
|
156
|
+
const source = resolveIconSource(state);
|
|
157
|
+
if (typeof source === "string") iconNode = UI.Icon({ name: source, size: iconSize, ...(isToggle ? { textColor: props.color, outline: true } : {}) });
|
|
158
|
+
else if (source != null) {
|
|
159
|
+
iconNode = JSswift.ui.slot(source, {
|
|
160
|
+
checked,
|
|
161
|
+
state,
|
|
162
|
+
indeterminate: state == null,
|
|
163
|
+
as: state === true ? "checkedIcon" : (state === false ? "uncheckedIcon" : "indeterminateIcon")
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
renderSlotToArray(null, "default", {}, iconNode).forEach((n) => indicatorHost.appendChild(n));
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (model) {
|
|
171
|
+
setInputState(model.get());
|
|
172
|
+
model.watch((v) => {
|
|
173
|
+
setInputState(v);
|
|
174
|
+
syncIndicator();
|
|
175
|
+
}, isRadio ? "UI.Radio:watch" : "UI.Checkbox:watch");
|
|
176
|
+
input.addEventListener("change", (e) => {
|
|
177
|
+
if (isRadio) {
|
|
178
|
+
if (input.checked) {
|
|
179
|
+
model.set(props.value);
|
|
180
|
+
props.onChange?.(props.value, e);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
const nextState = getInputState();
|
|
184
|
+
model.set(nextState);
|
|
185
|
+
props.onChange?.(nextState, e);
|
|
186
|
+
}
|
|
187
|
+
syncIndicator();
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
setInputState(isRadio ? (props.checked === true ? props.value : undefined) : props.checked);
|
|
191
|
+
input.addEventListener("change", (e) => {
|
|
192
|
+
if (isRadio) props.onChange?.(props.value, e);
|
|
193
|
+
else props.onChange?.(getInputState(), e);
|
|
194
|
+
syncIndicator();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (props.onInput) {
|
|
198
|
+
input.addEventListener("input", (e) => {
|
|
199
|
+
if (isRadio) props.onInput?.(props.value, e);
|
|
200
|
+
else props.onInput?.(getInputState(), e);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
syncIndicator();
|
|
205
|
+
uiRegisterShortcode(wrap, props, {
|
|
206
|
+
isEnabled: () => !input.disabled,
|
|
207
|
+
action: () => {
|
|
208
|
+
if (input.disabled) return false;
|
|
209
|
+
uiFocusShortcutTarget(input);
|
|
210
|
+
input.click();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return wrap;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
UI.Checkbox = (...args) => buildChoiceControl("checkbox", args);
|
|
217
|
+
|
|
218
|
+
// Esempio: JSswift.ui.Checkbox({ label: "Accetto", model: [get,set] })
|
|
219
|
+
|
|
220
|
+
UI.Radio = (...args) => buildChoiceControl("radio", args);
|
|
221
|
+
|
|
222
|
+
// Esempio: JSswift.ui.Radio({ name: "r1", value: "a", label: "A", model: [get,set] })
|
|
223
|
+
|
|
224
|
+
UI.Toggle = (...args) => {
|
|
225
|
+
const { props } = JSswift.uiNormalizeArgs(args);
|
|
226
|
+
const behavior = String(props.behavior ?? props.mode ?? props.type ?? "checkbox").toLowerCase() === "radio"
|
|
227
|
+
? "radio"
|
|
228
|
+
: "checkbox";
|
|
229
|
+
return buildChoiceControl(behavior, args, {
|
|
230
|
+
appearance: "toggle",
|
|
231
|
+
modelName: "UI.Toggle:model"
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// Esempio: JSswift.ui.Toggle({ label: "Attivo", model: [get,set] })
|
|
236
|
+
|
|
237
|
+
UI.Slider = (...args) => {
|
|
238
|
+
const { props, children } = JSswift.uiNormalizeArgs(args);
|
|
239
|
+
const slots = props.slots || {};
|
|
240
|
+
const boundValue = props.model || ((uiIsSignal(props.value) || uiIsRod(props.value)) ? props.value : null);
|
|
241
|
+
const model = resolveModel(boundValue, "UI.Slider:model");
|
|
242
|
+
|
|
243
|
+
const inputProps = JSswift.omit(props, [
|
|
244
|
+
"model", "value", "class", "style", "onChange", "onInput", "slots",
|
|
245
|
+
"label", "icon", "iconRight", "thumbIcon", "iconThumb", "pointIcon",
|
|
246
|
+
"markers", "markerLabels", "labelMarks", "leftLabel", "rightLabel",
|
|
247
|
+
"startLabel", "endLabel", "minLabel", "maxLabel", "withQItem", "qitem",
|
|
248
|
+
"item", "itemClass", "itemStyle", "showValue", "thumbLabel", "labelValue",
|
|
249
|
+
"selectionColor", "trackColor", "thumbColor", "inputClass", "readonly", "size"
|
|
250
|
+
]);
|
|
251
|
+
inputProps.type = "range";
|
|
252
|
+
inputProps.class = uiClass(["jss-slider-input", props.inputClass]);
|
|
253
|
+
const input = _.input(inputProps);
|
|
254
|
+
|
|
255
|
+
const sliderStyle = { ...(props.style || {}) };
|
|
256
|
+
const sizeValue = uiUnwrap(props.size);
|
|
257
|
+
if (sizeValue != null) {
|
|
258
|
+
const sliderSize = (typeof sizeValue === "string" && JSswift.uiSizes?.includes(sizeValue))
|
|
259
|
+
? `var(--jss-size-${sizeValue})`
|
|
260
|
+
: toCssSize(sizeValue);
|
|
261
|
+
sliderStyle["--jss-slider-size"] = sliderSize;
|
|
262
|
+
sliderStyle["--jss-slider-track-size"] = `max(4px, calc(${sliderSize} / 4))`;
|
|
263
|
+
sliderStyle["--jss-slider-box-height"] = `calc(${sliderSize} * 1.5)`;
|
|
264
|
+
sliderStyle["--jss-slider-marker-size"] = `max(6px, calc(${sliderSize} / 3))`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const wrap = _.label({
|
|
268
|
+
class: uiClass([
|
|
269
|
+
"jss-clear-set",
|
|
270
|
+
"jss-singularity-check",
|
|
271
|
+
"jss-slider-wrap",
|
|
272
|
+
uiWhen(props.dense, "dense"),
|
|
273
|
+
props.class
|
|
274
|
+
]),
|
|
275
|
+
style: sliderStyle
|
|
276
|
+
});
|
|
277
|
+
setPropertyProps(wrap, props);
|
|
278
|
+
|
|
279
|
+
const header = _.div({
|
|
280
|
+
class: "jss-slider-header"
|
|
281
|
+
});
|
|
282
|
+
const labelHost = _.span({
|
|
283
|
+
class: "jss-slider-label"
|
|
284
|
+
});
|
|
285
|
+
const valueHost = _.span({
|
|
286
|
+
class: "jss-slider-value"
|
|
287
|
+
});
|
|
288
|
+
header.append(labelHost, valueHost);
|
|
289
|
+
|
|
290
|
+
const body = _.div({
|
|
291
|
+
class: "jss-slider-body"
|
|
292
|
+
});
|
|
293
|
+
const startIconHost = _.span({
|
|
294
|
+
class: "jss-slider-icon jss-slider-icon-left"
|
|
295
|
+
});
|
|
296
|
+
const startLabelHost = _.span({
|
|
297
|
+
class: "jss-slider-edge-label jss-slider-edge-label-left"
|
|
298
|
+
});
|
|
299
|
+
const main = _.div({
|
|
300
|
+
class: "jss-slider-main"
|
|
301
|
+
});
|
|
302
|
+
const sliderBox = _.div({
|
|
303
|
+
class: "jss-slider-box",
|
|
304
|
+
style: {
|
|
305
|
+
minHeight: "var(--jss-slider-box-height, 36px)"
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
const rail = _.span({
|
|
309
|
+
class: "jss-slider-rail",
|
|
310
|
+
style: {
|
|
311
|
+
height: "var(--jss-slider-track-size, 6px)"
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
const selection = _.span({
|
|
315
|
+
class: "jss-slider-selection",
|
|
316
|
+
style: {
|
|
317
|
+
height: "var(--jss-slider-track-size, 6px)"
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
const thumb = _.span({
|
|
321
|
+
class: "jss-slider-thumb",
|
|
322
|
+
style: {
|
|
323
|
+
width: "var(--jss-slider-size, 24px)",
|
|
324
|
+
height: "var(--jss-slider-size, 24px)"
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
const thumbIconHost = _.span({
|
|
328
|
+
class: "jss-slider-thumb-icon"
|
|
329
|
+
});
|
|
330
|
+
const thumbLabelHost = _.span({
|
|
331
|
+
class: "jss-slider-thumb-label"
|
|
332
|
+
});
|
|
333
|
+
thumb.append(thumbLabelHost, thumbIconHost);
|
|
334
|
+
|
|
335
|
+
const markersHost = _.div({
|
|
336
|
+
class: "jss-slider-markers"
|
|
337
|
+
});
|
|
338
|
+
const endLabelHost = _.span({
|
|
339
|
+
class: "jss-slider-edge-label jss-slider-edge-label-right"
|
|
340
|
+
});
|
|
341
|
+
const endIconHost = _.span({
|
|
342
|
+
class: "jss-slider-icon jss-slider-icon-right"
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
sliderBox.append(rail, selection, thumb, input);
|
|
346
|
+
main.append(sliderBox, markersHost);
|
|
347
|
+
body.append(startIconHost, startLabelHost, main, endLabelHost, endIconHost);
|
|
348
|
+
wrap.append(header, body);
|
|
349
|
+
|
|
350
|
+
const clearHost = (host) => {
|
|
351
|
+
while (host.firstChild) host.removeChild(host.firstChild);
|
|
352
|
+
};
|
|
353
|
+
const renderInto = (host, nodes, display = "") => {
|
|
354
|
+
clearHost(host);
|
|
355
|
+
(nodes || []).forEach((n) => host.appendChild(n));
|
|
356
|
+
host.style.display = host.childNodes.length ? display : "none";
|
|
357
|
+
};
|
|
358
|
+
const unwrapSlotValue = (value) => (uiIsSignal(value) || uiIsRod(value) ? uiUnwrap(value) : value);
|
|
359
|
+
const asArray = (value, ctx = {}) => slotToArray(unwrapSlotValue(value), ctx);
|
|
360
|
+
const asIconArray = (value, as, ctx = {}) => {
|
|
361
|
+
const resolved = unwrapSlotValue(value);
|
|
362
|
+
if (resolved == null) return [];
|
|
363
|
+
if (typeof resolved === "string") return [UI.Icon({ name: resolved, size: props.iconSize ?? props.size ?? 16 })];
|
|
364
|
+
return asArray(resolved, { ...ctx, as });
|
|
365
|
+
};
|
|
366
|
+
const getNumber = (value, fallback) => {
|
|
367
|
+
const next = Number(value);
|
|
368
|
+
return Number.isFinite(next) ? next : fallback;
|
|
369
|
+
};
|
|
370
|
+
const getMin = () => getNumber(uiUnwrap(props.min), 0);
|
|
371
|
+
const getMax = () => {
|
|
372
|
+
const min = getMin();
|
|
373
|
+
const max = getNumber(uiUnwrap(props.max), 100);
|
|
374
|
+
return max < min ? min : max;
|
|
375
|
+
};
|
|
376
|
+
const getStep = () => {
|
|
377
|
+
const raw = uiUnwrap(props.step);
|
|
378
|
+
if (raw === "any") return "any";
|
|
379
|
+
const step = getNumber(raw, 1);
|
|
380
|
+
return step > 0 ? step : 1;
|
|
381
|
+
};
|
|
382
|
+
const getPrecision = (value) => {
|
|
383
|
+
if (value === "any") return 0;
|
|
384
|
+
const str = String(value);
|
|
385
|
+
if (str.includes("e-")) return Number(str.split("e-")[1] || 0);
|
|
386
|
+
const idx = str.indexOf(".");
|
|
387
|
+
return idx === -1 ? 0 : str.length - idx - 1;
|
|
388
|
+
};
|
|
389
|
+
const normalizeValue = (value) => {
|
|
390
|
+
const min = getMin();
|
|
391
|
+
const max = getMax();
|
|
392
|
+
const step = getStep();
|
|
393
|
+
let next = getNumber(value, min);
|
|
394
|
+
if (step !== "any") {
|
|
395
|
+
next = min + Math.round((next - min) / step) * step;
|
|
396
|
+
const precision = getPrecision(step);
|
|
397
|
+
if (precision > 0) next = Number(next.toFixed(precision));
|
|
398
|
+
}
|
|
399
|
+
return Math.min(max, Math.max(min, next));
|
|
400
|
+
};
|
|
401
|
+
const ratioFromValue = (value) => {
|
|
402
|
+
const min = getMin();
|
|
403
|
+
const max = getMax();
|
|
404
|
+
const span = max - min;
|
|
405
|
+
if (!span) return 0;
|
|
406
|
+
const raw = (normalizeValue(value) - min) / span;
|
|
407
|
+
const ratio = uiUnwrap(props.reverse) ? (1 - raw) : raw;
|
|
408
|
+
return Math.max(0, Math.min(1, ratio));
|
|
409
|
+
};
|
|
410
|
+
const [getValue, setValue] = JSswift.reactive.signal(normalizeValue(
|
|
411
|
+
model ? model.get() : (uiUnwrap(props.value) ?? uiUnwrap(props.min) ?? 0)
|
|
412
|
+
));
|
|
413
|
+
|
|
414
|
+
const setSliderValue = (raw, opts = {}) => {
|
|
415
|
+
const next = normalizeValue(raw);
|
|
416
|
+
if (getValue() !== next) setValue(next);
|
|
417
|
+
if (String(input.value) !== String(next)) input.value = String(next);
|
|
418
|
+
if (model && opts.fromModel !== true) model.set(next);
|
|
419
|
+
return next;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const getMarkerItems = () => {
|
|
423
|
+
const raw = uiUnwrap(props.markers);
|
|
424
|
+
const showLabels = !!uiUnwrap(props.markerLabels ?? props.labelMarks);
|
|
425
|
+
if (!raw) return [];
|
|
426
|
+
const min = getMin();
|
|
427
|
+
const max = getMax();
|
|
428
|
+
const normalizeMarker = (entry, index) => {
|
|
429
|
+
if (entry == null) return null;
|
|
430
|
+
if (typeof entry === "object" && !Array.isArray(entry)) {
|
|
431
|
+
const value = normalizeValue(entry.value ?? entry.position ?? entry.at ?? min);
|
|
432
|
+
return {
|
|
433
|
+
key: entry.key ?? `marker-${index}-${value}`,
|
|
434
|
+
value,
|
|
435
|
+
label: entry.label ?? (showLabels ? String(value) : null),
|
|
436
|
+
icon: entry.icon ?? null,
|
|
437
|
+
className: entry.class ?? entry.className ?? ""
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const value = normalizeValue(entry);
|
|
441
|
+
return {
|
|
442
|
+
key: `marker-${index}-${value}`,
|
|
443
|
+
value,
|
|
444
|
+
label: showLabels ? String(entry) : null,
|
|
445
|
+
icon: null,
|
|
446
|
+
className: ""
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
if (Array.isArray(raw)) {
|
|
451
|
+
return raw.map((entry, index) => normalizeMarker(entry, index)).filter(Boolean);
|
|
452
|
+
}
|
|
453
|
+
if (typeof raw === "number" && raw > 1) {
|
|
454
|
+
const count = Math.floor(raw);
|
|
455
|
+
const stepValue = count > 1 ? (max - min) / (count - 1) : 0;
|
|
456
|
+
return Array.from({ length: count }, (_, index) => normalizeMarker(min + (stepValue * index), index)).filter(Boolean);
|
|
457
|
+
}
|
|
458
|
+
if (raw === true) {
|
|
459
|
+
const step = getStep();
|
|
460
|
+
if (step === "any") {
|
|
461
|
+
return [normalizeMarker(min, 0), normalizeMarker(max, 1)].filter(Boolean);
|
|
462
|
+
}
|
|
463
|
+
const count = Math.floor((max - min) / step) + 1;
|
|
464
|
+
if (count > 24) {
|
|
465
|
+
return [normalizeMarker(min, 0), normalizeMarker((min + max) / 2, 1), normalizeMarker(max, 2)].filter(Boolean);
|
|
466
|
+
}
|
|
467
|
+
return Array.from({ length: count }, (_, index) => normalizeMarker(min + (step * index), index)).filter(Boolean);
|
|
468
|
+
}
|
|
469
|
+
if (typeof raw === "object") {
|
|
470
|
+
return Object.keys(raw).map((key, index) => normalizeMarker({
|
|
471
|
+
value: Number(key),
|
|
472
|
+
label: raw[key]
|
|
473
|
+
}, index)).filter(Boolean);
|
|
474
|
+
}
|
|
475
|
+
return [];
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const renderMarkers = () => {
|
|
479
|
+
clearHost(markersHost);
|
|
480
|
+
const markers = getMarkerItems();
|
|
481
|
+
markersHost.style.display = markers.length ? "block" : "none";
|
|
482
|
+
markersHost.style.minHeight = markers.some((marker) => marker.label != null && marker.label !== "") ? "30px" : "10px";
|
|
483
|
+
markers.forEach((marker) => {
|
|
484
|
+
const ratio = ratioFromValue(marker.value);
|
|
485
|
+
const active = uiUnwrap(props.reverse) ? getValue() <= marker.value : getValue() >= marker.value;
|
|
486
|
+
const item = _.button({
|
|
487
|
+
type: "button",
|
|
488
|
+
class: uiClass(["jss-slider-marker", marker.className, uiWhen(() => active, "active")]),
|
|
489
|
+
style: {
|
|
490
|
+
left: `${ratio * 100}%`,
|
|
491
|
+
color: active ? (uiUnwrap(props.color) || "var(--jss-primary)") : "var(--jss-muted)",
|
|
492
|
+
cursor: (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) ? "default" : "pointer"
|
|
493
|
+
},
|
|
494
|
+
disabled: !!uiUnwrap(props.disabled) || !!uiUnwrap(props.readonly),
|
|
495
|
+
onClick: () => {
|
|
496
|
+
if (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return;
|
|
497
|
+
const next = setSliderValue(marker.value);
|
|
498
|
+
props.onInput?.(next);
|
|
499
|
+
props.onChange?.(next);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
const markerCtx = {
|
|
503
|
+
marker,
|
|
504
|
+
active,
|
|
505
|
+
value: marker.value,
|
|
506
|
+
current: getValue(),
|
|
507
|
+
input
|
|
508
|
+
};
|
|
509
|
+
let markerNode = JSswift.ui.renderSlot(slots, "marker", markerCtx, null);
|
|
510
|
+
if (markerNode == null && marker.icon != null) {
|
|
511
|
+
markerNode = typeof marker.icon === "string"
|
|
512
|
+
? UI.Icon({ name: marker.icon, size: 12 })
|
|
513
|
+
: JSswift.ui.slot(marker.icon, { ...markerCtx, as: "marker" });
|
|
514
|
+
}
|
|
515
|
+
const markerTick = _.span({
|
|
516
|
+
class: "jss-slider-marker-tick",
|
|
517
|
+
style: {
|
|
518
|
+
width: "var(--jss-slider-marker-size, 8px)",
|
|
519
|
+
height: "var(--jss-slider-marker-size, 8px)",
|
|
520
|
+
background: active ? (uiUnwrap(props.color) || "var(--jss-primary)") : "var(--jss-border-color)"
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
const markerNodes = markerNode == null
|
|
524
|
+
? [markerTick]
|
|
525
|
+
: renderSlotToArray(null, "default", markerCtx, markerNode);
|
|
526
|
+
const labelNodes = marker.label == null
|
|
527
|
+
? renderSlotToArray(slots, "markerLabel", markerCtx, null)
|
|
528
|
+
: renderSlotToArray(slots, "markerLabel", markerCtx, marker.label);
|
|
529
|
+
markerNodes.forEach((node) => item.appendChild(node));
|
|
530
|
+
if (labelNodes.length) {
|
|
531
|
+
item.appendChild(_.span({
|
|
532
|
+
class: "jss-slider-marker-label"
|
|
533
|
+
}, ...labelNodes));
|
|
534
|
+
}
|
|
535
|
+
markersHost.appendChild(item);
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const renderHeader = () => {
|
|
540
|
+
const ctx = { value: getValue(), input, props };
|
|
541
|
+
let labelNodes = renderSlotToArray(slots, "label", ctx, unwrapSlotValue(props.label));
|
|
542
|
+
if (!labelNodes.length) labelNodes = renderSlotToArray(slots, "default", ctx, children);
|
|
543
|
+
renderInto(labelHost, labelNodes, "inline-flex");
|
|
544
|
+
|
|
545
|
+
const showValue = uiUnwrap(props.showValue);
|
|
546
|
+
const rawValueLabel = showValue === false
|
|
547
|
+
? null
|
|
548
|
+
: (unwrapSlotValue(props.labelValue) ?? getValue());
|
|
549
|
+
const valueNodes = showValue || unwrapSlotValue(props.labelValue) != null
|
|
550
|
+
? renderSlotToArray(slots, "value", ctx, rawValueLabel)
|
|
551
|
+
: [];
|
|
552
|
+
renderInto(valueHost, valueNodes, "inline-flex");
|
|
553
|
+
header.style.display = (labelHost.childNodes.length || valueHost.childNodes.length) ? "flex" : "none";
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const renderAddons = () => {
|
|
557
|
+
const ctx = { value: getValue(), input, props };
|
|
558
|
+
const leftIcon = renderSlotToArray(slots, "icon", ctx, null);
|
|
559
|
+
const leftIconNodes = leftIcon.length ? leftIcon : asIconArray(props.icon, "icon", ctx);
|
|
560
|
+
renderInto(startIconHost, leftIconNodes, "inline-flex");
|
|
561
|
+
|
|
562
|
+
const rightIcon = renderSlotToArray(slots, "iconRight", ctx, null);
|
|
563
|
+
const rightIconNodes = rightIcon.length ? rightIcon : asIconArray(props.iconRight, "iconRight", ctx);
|
|
564
|
+
renderInto(endIconHost, rightIconNodes, "inline-flex");
|
|
565
|
+
|
|
566
|
+
const leftLabelSource = unwrapSlotValue(props.startLabel ?? props.leftLabel ?? props.minLabel);
|
|
567
|
+
const rightLabelSource = unwrapSlotValue(props.endLabel ?? props.rightLabel ?? props.maxLabel);
|
|
568
|
+
renderInto(startLabelHost, renderSlotToArray(slots, "startLabel", ctx, leftLabelSource), "inline-flex");
|
|
569
|
+
renderInto(endLabelHost, renderSlotToArray(slots, "endLabel", ctx, rightLabelSource), "inline-flex");
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const renderThumb = () => {
|
|
573
|
+
const ctx = { value: getValue(), input, props };
|
|
574
|
+
const thumbIconNodes = renderSlotToArray(slots, "thumbIcon", ctx, null);
|
|
575
|
+
const thumbIconFallback = props.thumbIcon ?? props.iconThumb ?? props.pointIcon;
|
|
576
|
+
renderInto(
|
|
577
|
+
thumbIconHost,
|
|
578
|
+
thumbIconNodes.length ? thumbIconNodes : asIconArray(thumbIconFallback, "thumbIcon", ctx),
|
|
579
|
+
"inline-flex"
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
const rawThumbLabel = unwrapSlotValue(props.thumbLabel)
|
|
583
|
+
?? unwrapSlotValue(props.labelValue)
|
|
584
|
+
?? (uiUnwrap(props.showValue) ? String(getValue()) : null);
|
|
585
|
+
const thumbLabelNodes = renderSlotToArray(slots, "thumbLabel", ctx, rawThumbLabel);
|
|
586
|
+
renderInto(thumbLabelHost, thumbLabelNodes, "inline-flex");
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const syncVisualState = () => {
|
|
590
|
+
const value = normalizeValue(getValue());
|
|
591
|
+
const min = getMin();
|
|
592
|
+
const max = getMax();
|
|
593
|
+
const step = getStep();
|
|
594
|
+
const ratio = ratioFromValue(value);
|
|
595
|
+
const percent = `${ratio * 100}%`;
|
|
596
|
+
const color = uiUnwrap(props.color) || uiUnwrap(props.selectionColor) || "var(--jss-primary)";
|
|
597
|
+
const trackColor = uiUnwrap(props.trackColor) || "var(--jss-border-color)";
|
|
598
|
+
const thumbColor = uiUnwrap(props.thumbColor) || color;
|
|
599
|
+
const readonly = !!uiUnwrap(props.readonly);
|
|
600
|
+
const disabled = !!uiUnwrap(props.disabled);
|
|
601
|
+
|
|
602
|
+
input.min = String(min);
|
|
603
|
+
input.max = String(max);
|
|
604
|
+
input.step = step === "any" ? "any" : String(step);
|
|
605
|
+
input.disabled = disabled;
|
|
606
|
+
input.value = String(value);
|
|
607
|
+
|
|
608
|
+
rail.style.background = trackColor;
|
|
609
|
+
selection.style.background = color;
|
|
610
|
+
selection.style.width = percent;
|
|
611
|
+
thumb.style.left = percent;
|
|
612
|
+
thumb.style.borderColor = thumbColor;
|
|
613
|
+
thumb.style.color = thumbColor;
|
|
614
|
+
thumbLabelHost.style.background = color;
|
|
615
|
+
wrap.classList.toggle("is-disabled", disabled);
|
|
616
|
+
wrap.classList.toggle("is-readonly", readonly);
|
|
617
|
+
wrap.classList.toggle("has-markers", markersHost.childNodes.length > 0);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
if (model) {
|
|
621
|
+
setSliderValue(model.get(), { fromModel: true });
|
|
622
|
+
model.watch((v) => { setSliderValue(v, { fromModel: true }); }, "UI.Slider:watch");
|
|
623
|
+
} else if (uiIsReactive(props.value)) {
|
|
624
|
+
JSswift.reactive.effect(() => {
|
|
625
|
+
setSliderValue(uiUnwrap(props.value), { fromModel: true });
|
|
626
|
+
}, "UI.Slider:value");
|
|
627
|
+
} else {
|
|
628
|
+
setSliderValue(props.value ?? props.min ?? 0, { fromModel: true });
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
input.addEventListener("input", (e) => {
|
|
632
|
+
if (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) {
|
|
633
|
+
input.value = String(getValue());
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const next = setSliderValue(input.value);
|
|
637
|
+
props.onInput?.(next, e);
|
|
638
|
+
});
|
|
639
|
+
input.addEventListener("change", (e) => {
|
|
640
|
+
if (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) {
|
|
641
|
+
input.value = String(getValue());
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const next = setSliderValue(input.value);
|
|
645
|
+
props.onChange?.(next, e);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
JSswift.reactive.effect(() => {
|
|
649
|
+
renderHeader();
|
|
650
|
+
renderAddons();
|
|
651
|
+
renderThumb();
|
|
652
|
+
renderMarkers();
|
|
653
|
+
syncVisualState();
|
|
654
|
+
}, "UI.Slider:render");
|
|
655
|
+
|
|
656
|
+
wrap._input = input;
|
|
657
|
+
wrap._getValue = getValue;
|
|
658
|
+
wrap._setValue = (value) => setSliderValue(value);
|
|
659
|
+
|
|
660
|
+
if (props.withQItem || props.qitem || props.item === true) {
|
|
661
|
+
const item = UI.Item({
|
|
662
|
+
class: uiClass(["jss-slider-item", props.itemClass]),
|
|
663
|
+
style: props.itemStyle
|
|
664
|
+
}, wrap);
|
|
665
|
+
item._input = input;
|
|
666
|
+
item._slider = wrap;
|
|
667
|
+
item._getValue = getValue;
|
|
668
|
+
item._setValue = wrap._setValue;
|
|
669
|
+
return item;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return wrap;
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// Esempio: JSswift.ui.Slider({ min: 0, max: 10, model: [get,set] })
|
|
676
|
+
|
|
677
|
+
UI.Rating = (...args) => {
|
|
678
|
+
const { props, children } = JSswift.uiNormalizeArgs(args);
|
|
679
|
+
const slots = props.slots || {};
|
|
680
|
+
const boundValue = props.model || ((uiIsSignal(props.value) || uiIsRod(props.value)) ? props.value : null);
|
|
681
|
+
const model = resolveModel(boundValue, "UI.Rating:model");
|
|
682
|
+
const id = props.id || (`jss-rating-` + Math.random().toString(36).slice(2));
|
|
683
|
+
const inputProps = JSswift.omit(props, [
|
|
684
|
+
"model", "value", "max", "class", "style", "dense", "readonly", "disabled", "clearable",
|
|
685
|
+
"half", "allowHalf", "noDimming", "label", "slots", "onChange", "onInput", "onHover",
|
|
686
|
+
"icon", "checkedIcon", "uncheckedIcon", "halfIcon", "hoveredIcon", "iconSelected", "iconHalf",
|
|
687
|
+
"iconHovered", "iconOn", "iconOff", "iconSize", "color", "colorSelected", "colorHalf",
|
|
688
|
+
"colorHovered", "colorInactive", "activeColor", "halfColor", "hoverColor", "size", "gap",
|
|
689
|
+
"tabindex", "tabIndex", "inputClass"
|
|
690
|
+
]);
|
|
691
|
+
inputProps.type = "hidden";
|
|
692
|
+
inputProps.id = id;
|
|
693
|
+
if (props.name != null) inputProps.name = props.name;
|
|
694
|
+
inputProps.class = uiClass(["jss-rating-input", "jss-choice-input", props.inputClass]);
|
|
695
|
+
const input = _.input(inputProps);
|
|
696
|
+
|
|
697
|
+
const wrapProps = JSswift.omit(props, [
|
|
698
|
+
"model", "value", "max", "id", "name", "type", "class", "style", "dense", "readonly",
|
|
699
|
+
"disabled", "clearable", "half", "allowHalf", "noDimming", "label", "slots", "onChange",
|
|
700
|
+
"onInput", "onHover", "icon", "checkedIcon", "uncheckedIcon", "halfIcon", "hoveredIcon",
|
|
701
|
+
"iconSelected", "iconHalf", "iconHovered", "iconOn", "iconOff", "iconSize", "color",
|
|
702
|
+
"colorSelected", "colorHalf", "colorHovered", "colorInactive", "activeColor", "halfColor",
|
|
703
|
+
"hoverColor", "size", "gap", "tabindex", "tabIndex", "inputClass"
|
|
704
|
+
]);
|
|
705
|
+
wrapProps.class = uiClass([
|
|
706
|
+
"jss-clear-set",
|
|
707
|
+
"jss-singularity-check",
|
|
708
|
+
"jss-choice-wrap",
|
|
709
|
+
"jss-rating",
|
|
710
|
+
"jss-rating-wrap",
|
|
711
|
+
uiWhen(props.dense, "dense"),
|
|
712
|
+
props.class
|
|
713
|
+
]);
|
|
714
|
+
wrapProps.style = { ...(props.style || {}) };
|
|
715
|
+
const sizeValue = uiUnwrap(props.size);
|
|
716
|
+
if (sizeValue != null && !(typeof sizeValue === "string" && JSswift.uiSizes?.includes(sizeValue))) {
|
|
717
|
+
wrapProps.style["--jss-rating-size"] = toCssSize(sizeValue);
|
|
718
|
+
}
|
|
719
|
+
const gapValue = uiUnwrap(props.gap);
|
|
720
|
+
if (gapValue != null) {
|
|
721
|
+
wrapProps.style["--jss-rating-gap"] = toCssSize(gapValue);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const control = _.span({
|
|
725
|
+
class: "jss-rating-control",
|
|
726
|
+
onMouseleave: (e) => {
|
|
727
|
+
if (hoverValue == null) return;
|
|
728
|
+
hoverValue = null;
|
|
729
|
+
syncVisualState();
|
|
730
|
+
props.onHover?.(getValue(), e);
|
|
731
|
+
},
|
|
732
|
+
onKeydown: (e) => {
|
|
733
|
+
if (!isInteractive()) return;
|
|
734
|
+
const step = hasHalf() ? 0.5 : 1;
|
|
735
|
+
const current = getValue();
|
|
736
|
+
let next = current;
|
|
737
|
+
switch (e.key) {
|
|
738
|
+
case "ArrowRight":
|
|
739
|
+
case "ArrowUp":
|
|
740
|
+
next = current + step;
|
|
741
|
+
break;
|
|
742
|
+
case "ArrowLeft":
|
|
743
|
+
case "ArrowDown":
|
|
744
|
+
next = current - step;
|
|
745
|
+
break;
|
|
746
|
+
case "Home":
|
|
747
|
+
next = uiUnwrap(props.clearable) ? 0 : step;
|
|
748
|
+
break;
|
|
749
|
+
case "End":
|
|
750
|
+
next = getMax();
|
|
751
|
+
break;
|
|
752
|
+
case "Delete":
|
|
753
|
+
case "Backspace":
|
|
754
|
+
case "0":
|
|
755
|
+
if (!uiUnwrap(props.clearable)) return;
|
|
756
|
+
next = 0;
|
|
757
|
+
break;
|
|
758
|
+
case " ":
|
|
759
|
+
case "Enter":
|
|
760
|
+
next = current || (uiUnwrap(props.clearable) ? step : Math.max(step, 1));
|
|
761
|
+
break;
|
|
762
|
+
default:
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
e.preventDefault();
|
|
766
|
+
setRatingValue(next, e, { emit: true });
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
const labelHost = _.span({ class: "jss-choice-label jss-rating-label" });
|
|
770
|
+
const wrap = _.label(wrapProps, input, control, labelHost);
|
|
771
|
+
setPropertyProps(wrap, props);
|
|
772
|
+
|
|
773
|
+
const clearHost = (host) => {
|
|
774
|
+
while (host.firstChild) host.removeChild(host.firstChild);
|
|
775
|
+
};
|
|
776
|
+
const renderInto = (host, nodes, display = "") => {
|
|
777
|
+
clearHost(host);
|
|
778
|
+
(nodes || []).forEach((n) => host.appendChild(n));
|
|
779
|
+
host.style.display = host.childNodes.length ? display : "none";
|
|
780
|
+
};
|
|
781
|
+
const unwrapSlotValue = (value) => (uiIsSignal(value) || uiIsRod(value) ? uiUnwrap(value) : value);
|
|
782
|
+
const asArray = (value, ctx = {}) => slotToArray(unwrapSlotValue(value), ctx);
|
|
783
|
+
const asIconArray = (value, as, ctx = {}) => {
|
|
784
|
+
const resolved = unwrapSlotValue(value);
|
|
785
|
+
if (resolved == null) return [];
|
|
786
|
+
if (typeof resolved === "string") return [UI.Icon({ name: resolved, color: props.color, size: props.iconSize ?? props.size ?? 16 })];
|
|
787
|
+
return asArray(resolved, { ...ctx, as });
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const getMax = () => {
|
|
791
|
+
const value = Number(uiUnwrap(props.max) ?? 5);
|
|
792
|
+
return Number.isFinite(value) && value > 0 ? Math.max(1, Math.round(value)) : 5;
|
|
793
|
+
};
|
|
794
|
+
const hasHalf = () => !!uiUnwrap(props.half ?? props.allowHalf);
|
|
795
|
+
const normalizeValue = (value, max = getMax()) => {
|
|
796
|
+
let next = Number(uiUnwrap(value));
|
|
797
|
+
if (!Number.isFinite(next)) next = 0;
|
|
798
|
+
next = Math.min(max, Math.max(0, next));
|
|
799
|
+
const step = hasHalf() ? 0.5 : 1;
|
|
800
|
+
return Math.round(next / step) * step;
|
|
801
|
+
};
|
|
802
|
+
const isDisabled = () => !!uiUnwrap(props.disabled);
|
|
803
|
+
const isReadonly = () => !!uiUnwrap(props.readonly);
|
|
804
|
+
const isInteractive = () => !isDisabled() && !isReadonly();
|
|
805
|
+
const getValue = () => normalizeValue(model ? model.get() : localValue);
|
|
806
|
+
const updateInputValue = (value) => {
|
|
807
|
+
const stringValue = value > 0 ? String(value) : "";
|
|
808
|
+
input.value = stringValue;
|
|
809
|
+
input.setAttribute("value", stringValue);
|
|
810
|
+
input.disabled = isDisabled();
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
let localValue = normalizeValue(model ? model.get() : uiUnwrap(props.value));
|
|
814
|
+
let hoverValue = null;
|
|
815
|
+
let renderedMax = 0;
|
|
816
|
+
let items = [];
|
|
817
|
+
|
|
818
|
+
const getPointerValue = (index, event) => {
|
|
819
|
+
if (!hasHalf()) return normalizeValue(index);
|
|
820
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
821
|
+
const half = (event.clientX - rect.left) <= (rect.width / 2);
|
|
822
|
+
return normalizeValue(half ? index - 0.5 : index);
|
|
823
|
+
};
|
|
824
|
+
const getItemState = (displayValue, index) => {
|
|
825
|
+
if (displayValue >= index) return "full";
|
|
826
|
+
if (hasHalf() && displayValue >= (index - 0.5)) return "half";
|
|
827
|
+
return "empty";
|
|
828
|
+
};
|
|
829
|
+
const getStateColor = (state, hovered) => {
|
|
830
|
+
const selectedColor = uiUnwrap(props.colorSelected ?? props.activeColor ?? props.color ?? props.textColor) || "var(--jss-warning, #f0b429)";
|
|
831
|
+
const halfColor = uiUnwrap(props.colorHalf ?? props.halfColor ?? props.colorSelected ?? props.activeColor ?? props.color ?? props.textColor) || selectedColor;
|
|
832
|
+
const hoveredColor = uiUnwrap(props.colorHovered ?? props.hoverColor ?? props.colorSelected ?? props.activeColor ?? props.color ?? props.textColor) || selectedColor;
|
|
833
|
+
const inactiveColor = uiUnwrap(props.colorInactive ?? props.colorOff ?? props.color) || "var(--jss-muted)";
|
|
834
|
+
if (hovered) return hoveredColor;
|
|
835
|
+
if (state === "full") return selectedColor;
|
|
836
|
+
if (state === "half") return halfColor;
|
|
837
|
+
return inactiveColor;
|
|
838
|
+
};
|
|
839
|
+
const resolveItemNodes = (ctx) => {
|
|
840
|
+
const slotCandidates = [];
|
|
841
|
+
if (ctx.hovered && ctx.state !== "empty") slotCandidates.push("hoveredIcon", "iconHovered");
|
|
842
|
+
if (ctx.state === "full") slotCandidates.push("checkedIcon", "selectedIcon", "iconSelected", "iconOn");
|
|
843
|
+
else if (ctx.state === "half") slotCandidates.push("halfIcon", "iconHalf");
|
|
844
|
+
else slotCandidates.push("uncheckedIcon", "iconOff");
|
|
845
|
+
slotCandidates.push("icon", "item", "star");
|
|
846
|
+
for (const name of slotCandidates) {
|
|
847
|
+
const nodes = renderSlotToArray(slots, name, ctx, null);
|
|
848
|
+
if (nodes.length) return nodes;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
let source = null;
|
|
852
|
+
let as = "icon";
|
|
853
|
+
if (ctx.hovered && ctx.state !== "empty") {
|
|
854
|
+
source = props.hoveredIcon ?? props.iconHovered;
|
|
855
|
+
as = "hoveredIcon";
|
|
856
|
+
}
|
|
857
|
+
if (source == null && ctx.state === "full") {
|
|
858
|
+
source = props.checkedIcon ?? props.iconSelected ?? props.iconOn ?? props.icon ?? "star";
|
|
859
|
+
as = "checkedIcon";
|
|
860
|
+
} else if (source == null && ctx.state === "half") {
|
|
861
|
+
source = props.halfIcon ?? props.iconHalf ?? props.checkedIcon ?? props.iconSelected ?? props.icon ?? "star_half";
|
|
862
|
+
as = "halfIcon";
|
|
863
|
+
} else if (source == null) {
|
|
864
|
+
source = props.uncheckedIcon ?? props.iconOff ?? (uiUnwrap(props.noDimming) ? (props.icon ?? props.checkedIcon ?? props.iconSelected ?? "star") : "star_border");
|
|
865
|
+
as = "uncheckedIcon";
|
|
866
|
+
}
|
|
867
|
+
return asIconArray(source, as, ctx);
|
|
868
|
+
};
|
|
869
|
+
const ensureItems = () => {
|
|
870
|
+
const max = getMax();
|
|
871
|
+
if (renderedMax === max) return;
|
|
872
|
+
renderedMax = max;
|
|
873
|
+
items = [];
|
|
874
|
+
clearHost(control);
|
|
875
|
+
for (let index = 1; index <= max; index++) {
|
|
876
|
+
const iconHost = _.span({ class: "jss-rating-item-icon" });
|
|
877
|
+
const item = _.span({
|
|
878
|
+
class: "jss-rating-item",
|
|
879
|
+
role: "radio",
|
|
880
|
+
tabIndex: -1,
|
|
881
|
+
onMousemove: (e) => {
|
|
882
|
+
if (!isInteractive()) return;
|
|
883
|
+
const nextHover = getPointerValue(index, e);
|
|
884
|
+
if (hoverValue === nextHover) return;
|
|
885
|
+
hoverValue = nextHover;
|
|
886
|
+
syncVisualState();
|
|
887
|
+
props.onHover?.(nextHover, e);
|
|
888
|
+
},
|
|
889
|
+
onClick: (e) => {
|
|
890
|
+
if (!isInteractive()) return;
|
|
891
|
+
let nextValue = getPointerValue(index, e);
|
|
892
|
+
if (uiUnwrap(props.clearable) && nextValue === getValue()) nextValue = 0;
|
|
893
|
+
setRatingValue(nextValue, e, { emit: true });
|
|
894
|
+
}
|
|
895
|
+
}, iconHost);
|
|
896
|
+
items.push({ item, iconHost, index });
|
|
897
|
+
control.appendChild(item);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
const syncVisualState = () => {
|
|
901
|
+
ensureItems();
|
|
902
|
+
const max = getMax();
|
|
903
|
+
const value = getValue();
|
|
904
|
+
const displayValue = hoverValue == null ? value : normalizeValue(hoverValue, max);
|
|
905
|
+
const disabled = isDisabled();
|
|
906
|
+
const readonly = isReadonly();
|
|
907
|
+
const clearable = !!uiUnwrap(props.clearable);
|
|
908
|
+
const noDimming = !!uiUnwrap(props.noDimming);
|
|
909
|
+
|
|
910
|
+
wrap.classList.toggle("is-disabled", disabled);
|
|
911
|
+
wrap.classList.toggle("is-readonly", readonly);
|
|
912
|
+
wrap.classList.toggle("is-hovering", hoverValue != null);
|
|
913
|
+
wrap.classList.toggle("is-clearable", clearable);
|
|
914
|
+
wrap.classList.toggle("is-half", hasHalf());
|
|
915
|
+
wrap.classList.toggle("no-dimming", noDimming);
|
|
916
|
+
|
|
917
|
+
const tabindex = Number(uiUnwrap(props.tabindex ?? props.tabIndex) ?? 0);
|
|
918
|
+
control.tabIndex = disabled ? -1 : tabindex;
|
|
919
|
+
control.setAttribute("role", "slider");
|
|
920
|
+
control.setAttribute("aria-valuemin", "0");
|
|
921
|
+
control.setAttribute("aria-valuemax", String(max));
|
|
922
|
+
control.setAttribute("aria-valuenow", String(value));
|
|
923
|
+
control.setAttribute("aria-disabled", disabled ? "true" : "false");
|
|
924
|
+
control.setAttribute("aria-readonly", readonly ? "true" : "false");
|
|
925
|
+
|
|
926
|
+
updateInputValue(value);
|
|
927
|
+
|
|
928
|
+
const labelNodes = renderSlotToArray(
|
|
929
|
+
slots,
|
|
930
|
+
"label",
|
|
931
|
+
{ value, max, disabled, readonly, clearable },
|
|
932
|
+
props.label != null ? unwrapSlotValue(props.label) : children
|
|
933
|
+
);
|
|
934
|
+
renderInto(labelHost, labelNodes, "inline-flex");
|
|
935
|
+
|
|
936
|
+
items.forEach(({ item, iconHost, index }) => {
|
|
937
|
+
const state = getItemState(displayValue, index);
|
|
938
|
+
const hovered = hoverValue != null && state !== "empty";
|
|
939
|
+
const ctx = {
|
|
940
|
+
index,
|
|
941
|
+
max,
|
|
942
|
+
value,
|
|
943
|
+
displayValue,
|
|
944
|
+
state,
|
|
945
|
+
active: state !== "empty",
|
|
946
|
+
checked: state === "full",
|
|
947
|
+
half: state === "half",
|
|
948
|
+
hovered,
|
|
949
|
+
disabled,
|
|
950
|
+
readonly,
|
|
951
|
+
clearable,
|
|
952
|
+
setValue: (next) => setRatingValue(next, null, { emit: false })
|
|
953
|
+
};
|
|
954
|
+
item.className = uiClassStatic([
|
|
955
|
+
"jss-rating-item",
|
|
956
|
+
`is-${state}`,
|
|
957
|
+
uiWhen(hovered, "is-hovered")
|
|
958
|
+
]);
|
|
959
|
+
item.style.color = getStateColor(state, hovered);
|
|
960
|
+
item.style.opacity = state === "empty" && !noDimming ? "0.48" : "1";
|
|
961
|
+
item.setAttribute("aria-checked", String(value === index));
|
|
962
|
+
renderInto(iconHost, resolveItemNodes(ctx), "inline-flex");
|
|
963
|
+
});
|
|
964
|
+
};
|
|
965
|
+
const setRatingValue = (value, event, options = {}) => {
|
|
966
|
+
const normalized = normalizeValue(value);
|
|
967
|
+
const current = getValue();
|
|
968
|
+
localValue = normalized;
|
|
969
|
+
hoverValue = null;
|
|
970
|
+
if (model && options.fromModel !== true) model.set(normalized);
|
|
971
|
+
updateInputValue(normalized);
|
|
972
|
+
syncVisualState();
|
|
973
|
+
if (options.emit !== false && normalized !== current) {
|
|
974
|
+
props.onInput?.(normalized, event);
|
|
975
|
+
props.onChange?.(normalized, event);
|
|
976
|
+
}
|
|
977
|
+
return normalized;
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
wrap._input = input;
|
|
981
|
+
wrap._rating = control;
|
|
982
|
+
wrap._getValue = getValue;
|
|
983
|
+
wrap._setValue = (value) => setRatingValue(value, null, { emit: false });
|
|
984
|
+
|
|
985
|
+
if (model) {
|
|
986
|
+
model.watch((next) => {
|
|
987
|
+
const normalized = normalizeValue(next);
|
|
988
|
+
if (serializeValue(normalized) === serializeValue(localValue)) return;
|
|
989
|
+
localValue = normalized;
|
|
990
|
+
hoverValue = null;
|
|
991
|
+
updateInputValue(localValue);
|
|
992
|
+
syncVisualState();
|
|
993
|
+
}, "UI.Rating:watch");
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
JSswift.reactive.effect(() => {
|
|
997
|
+
if (!model && props.value != null) {
|
|
998
|
+
localValue = normalizeValue(uiUnwrap(props.value));
|
|
999
|
+
}
|
|
1000
|
+
syncVisualState();
|
|
1001
|
+
}, "UI.Rating:render");
|
|
1002
|
+
|
|
1003
|
+
return wrap;
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
// Esempio: JSswift.ui.Rating({ max: 5, model: [get,set] })
|
|
1007
|
+
|
|
1008
|
+
const uiCloneTimeParts = (value) => {
|
|
1009
|
+
if (!value) return null;
|
|
1010
|
+
return {
|
|
1011
|
+
hour: Number(value.hour) || 0,
|
|
1012
|
+
minute: Number(value.minute) || 0,
|
|
1013
|
+
second: Number(value.second) || 0
|
|
1014
|
+
};
|
|
1015
|
+
};
|
|
1016
|
+
const uiTimePad = (value) => String(Math.max(0, Number(value) || 0)).padStart(2, "0");
|
|
1017
|
+
const uiNormalizeTimeParts = (raw, options = {}) => {
|
|
1018
|
+
if (raw == null || raw === "") return null;
|
|
1019
|
+
let hour = null;
|
|
1020
|
+
let minute = null;
|
|
1021
|
+
let second = 0;
|
|
1022
|
+
let meridiem = null;
|
|
1023
|
+
|
|
1024
|
+
if (raw instanceof Date && !Number.isNaN(raw.getTime())) {
|
|
1025
|
+
hour = raw.getHours();
|
|
1026
|
+
minute = raw.getMinutes();
|
|
1027
|
+
second = raw.getSeconds();
|
|
1028
|
+
} else if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
1029
|
+
const total = Math.max(0, Math.floor(raw));
|
|
1030
|
+
hour = Math.floor(total / 3600) % 24;
|
|
1031
|
+
minute = Math.floor(total / 60) % 60;
|
|
1032
|
+
second = total % 60;
|
|
1033
|
+
} else if (typeof raw === "object") {
|
|
1034
|
+
if (raw.time != null || raw.value != null) return uiNormalizeTimeParts(raw.time ?? raw.value, options);
|
|
1035
|
+
hour = Number(raw.hour ?? raw.hours ?? raw.h ?? raw.hh);
|
|
1036
|
+
minute = Number(raw.minute ?? raw.minutes ?? raw.min ?? raw.mm ?? 0);
|
|
1037
|
+
second = Number(raw.second ?? raw.seconds ?? raw.sec ?? raw.ss ?? 0);
|
|
1038
|
+
meridiem = raw.meridiem ?? raw.ampm ?? raw.period ?? null;
|
|
1039
|
+
} else if (typeof raw === "string") {
|
|
1040
|
+
const value = raw.trim();
|
|
1041
|
+
if (!value) return null;
|
|
1042
|
+
const compact = value.match(/^(\d{1,2})(\d{2})(\d{2})?$/);
|
|
1043
|
+
const match = value.match(/(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*([ap]m)?/i);
|
|
1044
|
+
if (match) {
|
|
1045
|
+
hour = Number(match[1]);
|
|
1046
|
+
minute = Number(match[2]);
|
|
1047
|
+
second = match[3] != null ? Number(match[3]) : 0;
|
|
1048
|
+
meridiem = match[4] || null;
|
|
1049
|
+
} else if (compact) {
|
|
1050
|
+
hour = Number(compact[1]);
|
|
1051
|
+
minute = Number(compact[2]);
|
|
1052
|
+
second = compact[3] != null ? Number(compact[3]) : 0;
|
|
1053
|
+
} else {
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
} else {
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute) || !Number.isFinite(second)) return null;
|
|
1061
|
+
if (meridiem) {
|
|
1062
|
+
const lower = String(meridiem).toLowerCase();
|
|
1063
|
+
if (lower === "pm" && hour < 12) hour += 12;
|
|
1064
|
+
if (lower === "am" && hour === 12) hour = 0;
|
|
1065
|
+
}
|
|
1066
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) return null;
|
|
1067
|
+
|
|
1068
|
+
const normalized = {
|
|
1069
|
+
hour: Math.floor(hour),
|
|
1070
|
+
minute: Math.floor(minute),
|
|
1071
|
+
second: Math.floor(second)
|
|
1072
|
+
};
|
|
1073
|
+
return uiConstrainTimeParts(normalized, options);
|
|
1074
|
+
};
|
|
1075
|
+
const uiExportTimeValue = (value, options = {}) => {
|
|
1076
|
+
const parts = value && typeof value === "object" && value.hour != null
|
|
1077
|
+
? uiCloneTimeParts(value)
|
|
1078
|
+
: uiNormalizeTimeParts(value, options);
|
|
1079
|
+
if (!parts) return "";
|
|
1080
|
+
const withSeconds = options.withSeconds === true || Number(parts.second || 0) > 0;
|
|
1081
|
+
return `${uiTimePad(parts.hour)}:${uiTimePad(parts.minute)}${withSeconds ? `:${uiTimePad(parts.second)}` : ""}`;
|
|
1082
|
+
};
|
|
1083
|
+
const uiCompareTimeValue = (a, b) => {
|
|
1084
|
+
const av = uiExportTimeValue(a, { withSeconds: true });
|
|
1085
|
+
const bv = uiExportTimeValue(b, { withSeconds: true });
|
|
1086
|
+
if (!av && !bv) return 0;
|
|
1087
|
+
if (!av) return -1;
|
|
1088
|
+
if (!bv) return 1;
|
|
1089
|
+
return av < bv ? -1 : (av > bv ? 1 : 0);
|
|
1090
|
+
};
|
|
1091
|
+
function uiConstrainTimeParts(value, options = {}) {
|
|
1092
|
+
if (!value) return null;
|
|
1093
|
+
const out = uiCloneTimeParts(value);
|
|
1094
|
+
const min = options.min != null ? uiNormalizeTimeParts(options.min, { withSeconds: true }) : null;
|
|
1095
|
+
const max = options.max != null ? uiNormalizeTimeParts(options.max, { withSeconds: true }) : null;
|
|
1096
|
+
if (min && uiCompareTimeValue(out, min) < 0) return uiCloneTimeParts(min);
|
|
1097
|
+
if (max && uiCompareTimeValue(out, max) > 0) return uiCloneTimeParts(max);
|
|
1098
|
+
return out;
|
|
1099
|
+
}
|
|
1100
|
+
const uiExtractTimeFromValue = (raw, options = {}) => {
|
|
1101
|
+
if (raw == null || raw === "") return null;
|
|
1102
|
+
if (raw instanceof Date) return uiNormalizeTimeParts(raw, options);
|
|
1103
|
+
if (typeof raw === "object") return uiNormalizeTimeParts(raw.time != null ? raw.time : raw, options);
|
|
1104
|
+
if (typeof raw === "string" || typeof raw === "number") return uiNormalizeTimeParts(raw, options);
|
|
1105
|
+
return null;
|
|
1106
|
+
};
|
|
1107
|
+
const uiFormatTimeDisplay = (value, options = {}) => {
|
|
1108
|
+
const parts = uiNormalizeTimeParts(value, options);
|
|
1109
|
+
if (!parts) return "";
|
|
1110
|
+
const withSeconds = options.withSeconds === true || Number(parts.second || 0) > 0;
|
|
1111
|
+
const use12h = !!uiUnwrap(options.use12h ?? options.ampm);
|
|
1112
|
+
if (!use12h) {
|
|
1113
|
+
return `${uiTimePad(parts.hour)}:${uiTimePad(parts.minute)}${withSeconds ? `:${uiTimePad(parts.second)}` : ""}`;
|
|
1114
|
+
}
|
|
1115
|
+
const meridiem = parts.hour >= 12 ? "PM" : "AM";
|
|
1116
|
+
const hour = parts.hour % 12 || 12;
|
|
1117
|
+
return `${uiTimePad(hour)}:${uiTimePad(parts.minute)}${withSeconds ? `:${uiTimePad(parts.second)}` : ""} ${meridiem}`;
|
|
1118
|
+
};
|
|
1119
|
+
const uiMergeDateAndTime = (dateValue, timeValue, options = {}) => {
|
|
1120
|
+
if (!dateValue) return "";
|
|
1121
|
+
const time = uiExportTimeValue(timeValue, options);
|
|
1122
|
+
return time ? `${dateValue}T${time}` : dateValue;
|
|
1123
|
+
};
|
|
1124
|
+
const uiBuildTimeSteps = (step, max) => {
|
|
1125
|
+
const safeStep = Math.max(1, Math.min(max + 1, Math.round(Number(step) || 1)));
|
|
1126
|
+
const out = [];
|
|
1127
|
+
for (let value = 0; value <= max; value += safeStep) out.push(value);
|
|
1128
|
+
return out;
|
|
1129
|
+
};
|
|
1130
|
+
const uiCreateTimePickerSection = (config = {}) => {
|
|
1131
|
+
const {
|
|
1132
|
+
props = {},
|
|
1133
|
+
slots = {},
|
|
1134
|
+
value = null,
|
|
1135
|
+
withSeconds = false,
|
|
1136
|
+
minuteStep = 5,
|
|
1137
|
+
secondStep = 5,
|
|
1138
|
+
disabled = false,
|
|
1139
|
+
readonly = false,
|
|
1140
|
+
embedded = false,
|
|
1141
|
+
className = "",
|
|
1142
|
+
slotPrefix = "",
|
|
1143
|
+
shortcuts = null,
|
|
1144
|
+
onSelect = null
|
|
1145
|
+
} = config;
|
|
1146
|
+
const selectedValue = uiNormalizeTimeParts(value, { withSeconds, min: props.min, max: props.max });
|
|
1147
|
+
const baseParts = selectedValue
|
|
1148
|
+
|| uiNormalizeTimeParts(new Date(), { withSeconds, min: props.min, max: props.max })
|
|
1149
|
+
|| { hour: 0, minute: 0, second: 0 };
|
|
1150
|
+
const selectedParts = selectedValue ? uiCloneTimeParts(selectedValue) : null;
|
|
1151
|
+
const prefixed = (name) => slotPrefix ? `${slotPrefix}${name[0].toUpperCase()}${name.slice(1)}` : null;
|
|
1152
|
+
const renderNamedSlot = (name, ctx, fallback) => {
|
|
1153
|
+
const candidates = [prefixed(name), name].filter(Boolean);
|
|
1154
|
+
for (const candidate of candidates) {
|
|
1155
|
+
const slotValue = JSswift.ui.getSlot(slots, candidate);
|
|
1156
|
+
if (slotValue == null) continue;
|
|
1157
|
+
const rendered = JSswift.ui.renderSlot(slots, candidate, ctx, fallback);
|
|
1158
|
+
if (rendered != null) return rendered;
|
|
1159
|
+
}
|
|
1160
|
+
return JSswift.ui.slot(fallback, ctx);
|
|
1161
|
+
};
|
|
1162
|
+
const renderPointNodes = (ctx) => {
|
|
1163
|
+
let pointNode = renderNamedSlot("point", ctx, null);
|
|
1164
|
+
if (pointNode == null && config.pointIcon != null && ctx.selected) {
|
|
1165
|
+
pointNode = typeof config.pointIcon === "string"
|
|
1166
|
+
? UI.Icon({ name: config.pointIcon, size: embedded ? 10 : 12 })
|
|
1167
|
+
: JSswift.ui.slot(config.pointIcon, ctx);
|
|
1168
|
+
}
|
|
1169
|
+
return renderSlotToArray(null, "default", ctx, pointNode);
|
|
1170
|
+
};
|
|
1171
|
+
const makeRequestedValue = (part, optionValue) => {
|
|
1172
|
+
const next = {
|
|
1173
|
+
hour: baseParts.hour,
|
|
1174
|
+
minute: baseParts.minute,
|
|
1175
|
+
second: withSeconds ? baseParts.second : 0
|
|
1176
|
+
};
|
|
1177
|
+
next[part] = optionValue;
|
|
1178
|
+
return next;
|
|
1179
|
+
};
|
|
1180
|
+
const section = _.div({
|
|
1181
|
+
class: uiClass([
|
|
1182
|
+
"jss-time-section",
|
|
1183
|
+
uiWhen(embedded, "is-embedded"),
|
|
1184
|
+
uiWhen(withSeconds, "has-seconds"),
|
|
1185
|
+
className
|
|
1186
|
+
])
|
|
1187
|
+
});
|
|
1188
|
+
const shortcutList = uiUnwrap(shortcuts) || [];
|
|
1189
|
+
if (Array.isArray(shortcutList) && shortcutList.length) {
|
|
1190
|
+
const shortcutWrap = _.div({ class: "jss-time-shortcuts" });
|
|
1191
|
+
shortcutList.forEach((item, index) => {
|
|
1192
|
+
if (!item) return;
|
|
1193
|
+
const rawValue = typeof item.value === "function"
|
|
1194
|
+
? item.value({ now: uiExportTimeValue(new Date(), { withSeconds }) })
|
|
1195
|
+
: item.value;
|
|
1196
|
+
const normalizedValue = uiNormalizeTimeParts(rawValue, { withSeconds, min: props.min, max: props.max });
|
|
1197
|
+
if (!normalizedValue) return;
|
|
1198
|
+
const ctx = {
|
|
1199
|
+
item,
|
|
1200
|
+
index,
|
|
1201
|
+
value: uiExportTimeValue(normalizedValue, { withSeconds }),
|
|
1202
|
+
displayValue: uiFormatTimeDisplay(normalizedValue, { withSeconds, use12h: props.use12h ?? props.ampm })
|
|
1203
|
+
};
|
|
1204
|
+
const labelNode = renderNamedSlot("shortcut", ctx, item.label ?? item.text ?? ctx.displayValue);
|
|
1205
|
+
shortcutWrap.appendChild(_.button({
|
|
1206
|
+
type: "button",
|
|
1207
|
+
class: uiClass(["jss-time-shortcut", uiWhen(!!selectedParts && uiCompareTimeValue(normalizedValue, selectedParts) === 0, "is-selected")]),
|
|
1208
|
+
disabled: disabled || readonly,
|
|
1209
|
+
onClick: () => onSelect?.(normalizedValue, { shortcut: item, index })
|
|
1210
|
+
}, ...renderSlotToArray(null, "default", ctx, labelNode)));
|
|
1211
|
+
});
|
|
1212
|
+
if (shortcutWrap.childNodes.length) section.appendChild(shortcutWrap);
|
|
1213
|
+
}
|
|
1214
|
+
const columns = _.div({ class: "jss-time-columns" });
|
|
1215
|
+
const createColumn = (title, part, values) => {
|
|
1216
|
+
const column = _.div({ class: "jss-time-column", "data-time-part": part });
|
|
1217
|
+
const optionsWrap = _.div({ class: "jss-time-options" });
|
|
1218
|
+
values.forEach((optionValue) => {
|
|
1219
|
+
const requested = makeRequestedValue(part, optionValue);
|
|
1220
|
+
const normalized = uiConstrainTimeParts(requested, { withSeconds, min: props.min, max: props.max });
|
|
1221
|
+
const optionDisabled = disabled || readonly || uiCompareTimeValue(normalized, requested) !== 0;
|
|
1222
|
+
const selected = !!selectedParts && selectedParts[part] === optionValue;
|
|
1223
|
+
const ctx = {
|
|
1224
|
+
part,
|
|
1225
|
+
selected,
|
|
1226
|
+
disabled: optionDisabled,
|
|
1227
|
+
value: optionValue,
|
|
1228
|
+
label: uiTimePad(optionValue),
|
|
1229
|
+
timeValue: uiExportTimeValue(normalized, { withSeconds }),
|
|
1230
|
+
displayValue: uiFormatTimeDisplay(normalized, { withSeconds, use12h: props.use12h ?? props.ampm })
|
|
1231
|
+
};
|
|
1232
|
+
const optionNode = renderNamedSlot("option", ctx, ctx.label);
|
|
1233
|
+
const pointNodes = selected ? renderPointNodes(ctx) : [];
|
|
1234
|
+
optionsWrap.appendChild(_.button({
|
|
1235
|
+
type: "button",
|
|
1236
|
+
class: uiClass(["jss-time-option", uiWhen(selected, "is-selected"), uiWhen(optionDisabled, "is-disabled")]),
|
|
1237
|
+
disabled: optionDisabled,
|
|
1238
|
+
onClick: () => onSelect?.(normalized, { part, optionValue })
|
|
1239
|
+
},
|
|
1240
|
+
_.span({ class: "jss-time-option-label" }, ...renderSlotToArray(null, "default", ctx, optionNode)),
|
|
1241
|
+
pointNodes.length ? _.span({ class: "jss-time-option-point" }, ...pointNodes) : null
|
|
1242
|
+
));
|
|
1243
|
+
});
|
|
1244
|
+
column.appendChild(optionsWrap);
|
|
1245
|
+
return column;
|
|
1246
|
+
};
|
|
1247
|
+
columns.appendChild(createColumn(uiUnwrap(props.hoursLabel) || "Ore", "hour", uiBuildTimeSteps(1, 23)));
|
|
1248
|
+
columns.appendChild(createColumn(uiUnwrap(props.minutesLabel) || "Min", "minute", uiBuildTimeSteps(minuteStep, 59)));
|
|
1249
|
+
if (withSeconds) {
|
|
1250
|
+
columns.appendChild(createColumn(uiUnwrap(props.secondsLabel) || "Sec", "second", uiBuildTimeSteps(secondStep, 59)));
|
|
1251
|
+
}
|
|
1252
|
+
section.appendChild(columns);
|
|
1253
|
+
return section;
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
const uiCaptureTimeColumnScrollState = (root) => {
|
|
1257
|
+
if (!root) return null;
|
|
1258
|
+
const state = {};
|
|
1259
|
+
root.querySelectorAll(".jss-time-column").forEach((column, index) => {
|
|
1260
|
+
const key = column.getAttribute("data-time-part") || String(index);
|
|
1261
|
+
state[key] = column.scrollTop;
|
|
1262
|
+
});
|
|
1263
|
+
return state;
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
const uiCenterTimeColumnsToSelection = (root, options = {}) => {
|
|
1267
|
+
if (!root) return;
|
|
1268
|
+
const scrollState = options.scrollState || null;
|
|
1269
|
+
const behavior = options.behavior ?? "smooth";
|
|
1270
|
+
root.querySelectorAll(".jss-time-column").forEach((column, index) => {
|
|
1271
|
+
const key = column.getAttribute("data-time-part") || String(index);
|
|
1272
|
+
const previousTop = scrollState && Number.isFinite(scrollState[key]) ? scrollState[key] : null;
|
|
1273
|
+
if (previousTop != null) column.scrollTop = previousTop;
|
|
1274
|
+
const selectedOption = column.querySelector(".jss-time-option.is-selected");
|
|
1275
|
+
if (!selectedOption) return;
|
|
1276
|
+
const maxTop = Math.max(0, column.scrollHeight - column.clientHeight);
|
|
1277
|
+
const targetTop = selectedOption.offsetTop - ((column.clientHeight - selectedOption.offsetHeight) / 2);
|
|
1278
|
+
const nextTop = Math.max(0, Math.min(maxTop, targetTop));
|
|
1279
|
+
if (Math.abs(column.scrollTop - nextTop) < 1) {
|
|
1280
|
+
column.scrollTop = nextTop;
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (behavior && typeof column.scrollTo === "function") {
|
|
1284
|
+
column.scrollTo({ top: nextTop, behavior });
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
column.scrollTop = nextTop;
|
|
1288
|
+
});
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
const buildDateControl = (args, options = {}) => {
|
|
1292
|
+
const { props, children } = JSswift.uiNormalizeArgs(args);
|
|
1293
|
+
const isCalendar = options.calendar === true;
|
|
1294
|
+
const slots = props.slots || {};
|
|
1295
|
+
const sizeValue = uiComputed(props.size, () => {
|
|
1296
|
+
const value = String(uiUnwrap(props.size) || "").toLowerCase();
|
|
1297
|
+
return ["xs", "sm", "md", "lg", "xl"].includes(value) ? `jss-size-${value}` : "";
|
|
1298
|
+
});
|
|
1299
|
+
const requestedMode = String(
|
|
1300
|
+
uiUnwrap(
|
|
1301
|
+
props.mode
|
|
1302
|
+
?? (uiUnwrap(props.rangeMultiple ?? props.multipleRange ?? props.multiRange) ? "range-multiple" : null)
|
|
1303
|
+
?? ((props.range && props.multiple) ? "range-multiple" : (props.range ? "range" : (props.multiple ? "multiple" : "single")))
|
|
1304
|
+
)
|
|
1305
|
+
).toLowerCase();
|
|
1306
|
+
const mode = ["range-multiple", "multiple-range", "rangemultiple", "multiplerange", "range_multiple", "multiple_range", "multi-range", "multi_range", "ranges"]
|
|
1307
|
+
.includes(requestedMode)
|
|
1308
|
+
? "range-multiple"
|
|
1309
|
+
: (requestedMode === "range"
|
|
1310
|
+
? "range"
|
|
1311
|
+
: (requestedMode === "multiple" ? "multiple" : "single"));
|
|
1312
|
+
const valueBinding = props.model || ((uiIsSignal(props.value) || uiIsRod(props.value)) ? props.value : null);
|
|
1313
|
+
const model = resolveModel(valueBinding, isCalendar ? "UI.Calendar:model" : "UI.Datepicker:model");
|
|
1314
|
+
const initialRawValue = model ? model.get() : uiUnwrap(props.value);
|
|
1315
|
+
const rangeAsArray = uiUnwrap(props.rangeAs ?? props.rangeModel) === "array" || Array.isArray(initialRawValue);
|
|
1316
|
+
const rangeMultipleAsArray = uiUnwrap(props.rangeMultipleAs ?? props.multipleRangeAs) === "array"
|
|
1317
|
+
|| (Array.isArray(initialRawValue) && Array.isArray(initialRawValue[0]));
|
|
1318
|
+
const isTimeEnabled = () => mode === "single" && !!uiUnwrap(props.withTime ?? props.time ?? props.timePicker ?? props.enableTime);
|
|
1319
|
+
const getTimeOptions = () => {
|
|
1320
|
+
const detectedTime = uiExportTimeValue(
|
|
1321
|
+
uiExtractTimeFromValue(uiUnwrap(props.timeValue) ?? (model ? model.get() : uiUnwrap(props.value)) ?? initialRawValue, { withSeconds: true }),
|
|
1322
|
+
{ withSeconds: true }
|
|
1323
|
+
);
|
|
1324
|
+
return {
|
|
1325
|
+
withSeconds: !!uiUnwrap(props.timeWithSeconds ?? props.withSeconds ?? props.showSeconds)
|
|
1326
|
+
|| /:\d{2}:\d{2}$/.test(detectedTime)
|
|
1327
|
+
|| /:\d{2}:\d{2}$/.test(uiUnwrap(props.timeFormat) || ""),
|
|
1328
|
+
min: uiUnwrap(props.timeMin ?? props.minTime),
|
|
1329
|
+
max: uiUnwrap(props.timeMax ?? props.maxTime),
|
|
1330
|
+
use12h: uiUnwrap(props.time12h ?? props.use12h ?? props.ampm)
|
|
1331
|
+
};
|
|
1332
|
+
};
|
|
1333
|
+
const getTimeMinuteStep = () => {
|
|
1334
|
+
const raw = Number(uiUnwrap(props.timeMinuteStep ?? props.minuteStep ?? 5));
|
|
1335
|
+
return Math.max(1, Math.min(30, Number.isFinite(raw) ? raw : 5));
|
|
1336
|
+
};
|
|
1337
|
+
const getTimeSecondStep = () => {
|
|
1338
|
+
const raw = Number(uiUnwrap(props.timeSecondStep ?? props.secondStep ?? 5));
|
|
1339
|
+
return Math.max(1, Math.min(30, Number.isFinite(raw) ? raw : 5));
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1343
|
+
const createDate = (year, month, day) => {
|
|
1344
|
+
const date = new Date(year, month, day);
|
|
1345
|
+
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
|
|
1346
|
+
return date;
|
|
1347
|
+
};
|
|
1348
|
+
const toIsoDate = (date) => {
|
|
1349
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;
|
|
1350
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
1351
|
+
};
|
|
1352
|
+
const fromIsoDate = (iso) => {
|
|
1353
|
+
if (typeof iso !== "string") return null;
|
|
1354
|
+
const match = iso.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
1355
|
+
if (!match) return null;
|
|
1356
|
+
const year = Number(match[1]);
|
|
1357
|
+
const month = Number(match[2]) - 1;
|
|
1358
|
+
const day = Number(match[3]);
|
|
1359
|
+
const date = new Date(year, month, day);
|
|
1360
|
+
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
|
|
1361
|
+
return date;
|
|
1362
|
+
};
|
|
1363
|
+
const shiftDays = (iso, amount) => {
|
|
1364
|
+
const date = fromIsoDate(iso);
|
|
1365
|
+
if (!date) return null;
|
|
1366
|
+
date.setDate(date.getDate() + amount);
|
|
1367
|
+
return toIsoDate(date);
|
|
1368
|
+
};
|
|
1369
|
+
const shiftMonths = (date, amount) => new Date(date.getFullYear(), date.getMonth() + amount, 1);
|
|
1370
|
+
const monthStart = (value) => {
|
|
1371
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return new Date(value.getFullYear(), value.getMonth(), 1);
|
|
1372
|
+
const date = normalizeDateOnly(value);
|
|
1373
|
+
const parsed = fromIsoDate(date);
|
|
1374
|
+
return parsed ? new Date(parsed.getFullYear(), parsed.getMonth(), 1) : new Date();
|
|
1375
|
+
};
|
|
1376
|
+
const todayIso = () => toIsoDate(new Date());
|
|
1377
|
+
const isSameIso = (a, b) => !!a && !!b && a === b;
|
|
1378
|
+
const compareIso = (a, b) => {
|
|
1379
|
+
if (a == null && b == null) return 0;
|
|
1380
|
+
if (a == null) return -1;
|
|
1381
|
+
if (b == null) return 1;
|
|
1382
|
+
return a < b ? -1 : (a > b ? 1 : 0);
|
|
1383
|
+
};
|
|
1384
|
+
const diffDays = (a, b) => {
|
|
1385
|
+
const da = fromIsoDate(a);
|
|
1386
|
+
const db = fromIsoDate(b);
|
|
1387
|
+
if (!da || !db) return 0;
|
|
1388
|
+
return Math.round((db.getTime() - da.getTime()) / 86400000);
|
|
1389
|
+
};
|
|
1390
|
+
const ensureArray = (value) => Array.isArray(value) ? value.slice() : (value == null || value === "" ? [] : [value]);
|
|
1391
|
+
const cloneRangeValue = (value) => ({ from: value?.from || null, to: value?.to || null });
|
|
1392
|
+
const cloneRangeList = (value) => Array.isArray(value) ? value.map(cloneRangeValue) : [];
|
|
1393
|
+
const exportRangeValue = (value) => {
|
|
1394
|
+
const out = cloneRangeValue(value);
|
|
1395
|
+
return rangeAsArray ? [out.from, out.to] : out;
|
|
1396
|
+
};
|
|
1397
|
+
const exportRangeMultipleValue = (value) => cloneRangeList(value).map((item) => (
|
|
1398
|
+
rangeMultipleAsArray ? [item.from, item.to] : item
|
|
1399
|
+
));
|
|
1400
|
+
const cloneValue = (value) => {
|
|
1401
|
+
if (mode === "multiple") return Array.isArray(value) ? value.slice() : [];
|
|
1402
|
+
if (mode === "range") return cloneRangeValue(value);
|
|
1403
|
+
if (mode === "range-multiple") return cloneRangeList(value);
|
|
1404
|
+
return value || null;
|
|
1405
|
+
};
|
|
1406
|
+
const exportValue = (value) => {
|
|
1407
|
+
if (mode === "multiple") return Array.isArray(value) ? value.slice() : [];
|
|
1408
|
+
if (mode === "range") return exportRangeValue(value);
|
|
1409
|
+
if (mode === "range-multiple") return exportRangeMultipleValue(value);
|
|
1410
|
+
return value || "";
|
|
1411
|
+
};
|
|
1412
|
+
const serializeValue = (value) => JSON.stringify(exportValue(value));
|
|
1413
|
+
const normalizeDateOnly = (raw) => {
|
|
1414
|
+
if (raw == null || raw === "") return null;
|
|
1415
|
+
if (raw instanceof Date) return toIsoDate(raw);
|
|
1416
|
+
if (typeof raw === "number") return toIsoDate(new Date(raw));
|
|
1417
|
+
if (typeof raw !== "string") {
|
|
1418
|
+
if (typeof raw === "object" && raw.date != null) return normalizeDateOnly(raw.date);
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
const value = raw.trim();
|
|
1422
|
+
if (!value) return null;
|
|
1423
|
+
const isoLike = value.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:$|[T\s])/);
|
|
1424
|
+
if (isoLike) {
|
|
1425
|
+
const date = createDate(Number(isoLike[1]), Number(isoLike[2]) - 1, Number(isoLike[3]));
|
|
1426
|
+
return toIsoDate(date);
|
|
1427
|
+
}
|
|
1428
|
+
const euroLike = value.match(/^(\d{1,2})[\/.\-](\d{1,2})[\/.\-](\d{2,4})$/);
|
|
1429
|
+
if (euroLike) {
|
|
1430
|
+
const year = euroLike[3].length === 2 ? (2000 + Number(euroLike[3])) : Number(euroLike[3]);
|
|
1431
|
+
const date = createDate(year, Number(euroLike[2]) - 1, Number(euroLike[1]));
|
|
1432
|
+
return toIsoDate(date);
|
|
1433
|
+
}
|
|
1434
|
+
if (/^\d{1,4}$/.test(value)) return null;
|
|
1435
|
+
const parsed = new Date(value);
|
|
1436
|
+
if (Number.isNaN(parsed.getTime())) return null;
|
|
1437
|
+
const explicitYear = value.match(/(?:^|[^\d])(\d{4})(?!\d)/);
|
|
1438
|
+
if (explicitYear && parsed.getFullYear() !== Number(explicitYear[1])) return null;
|
|
1439
|
+
return toIsoDate(parsed);
|
|
1440
|
+
};
|
|
1441
|
+
const normalizeRangeValue = (raw) => {
|
|
1442
|
+
if (raw == null || raw === "") return { from: null, to: null };
|
|
1443
|
+
let from = null;
|
|
1444
|
+
let to = null;
|
|
1445
|
+
if (Array.isArray(raw)) {
|
|
1446
|
+
from = normalizeDateOnly(raw[0]);
|
|
1447
|
+
to = normalizeDateOnly(raw[1]);
|
|
1448
|
+
} else if (typeof raw === "object") {
|
|
1449
|
+
from = normalizeDateOnly(raw.from ?? raw.start ?? raw.dateFrom ?? raw.departure);
|
|
1450
|
+
to = normalizeDateOnly(raw.to ?? raw.end ?? raw.dateTo ?? raw.return);
|
|
1451
|
+
} else {
|
|
1452
|
+
const found = String(raw).match(/\d{4}-\d{1,2}-\d{1,2}|\d{1,2}[\/.\-]\d{1,2}[\/.\-]\d{2,4}/g) || [];
|
|
1453
|
+
from = normalizeDateOnly(found[0]);
|
|
1454
|
+
to = normalizeDateOnly(found[1]);
|
|
1455
|
+
}
|
|
1456
|
+
if (from && to && from > to) [from, to] = [to, from];
|
|
1457
|
+
return { from, to };
|
|
1458
|
+
};
|
|
1459
|
+
const normalizeRangeMultipleValue = (raw) => {
|
|
1460
|
+
const out = [];
|
|
1461
|
+
const seen = new Set();
|
|
1462
|
+
const pushRange = (value) => {
|
|
1463
|
+
const normalized = normalizeRangeValue(value);
|
|
1464
|
+
if (!normalized.from) return;
|
|
1465
|
+
const key = `${normalized.from || ""}|${normalized.to || ""}`;
|
|
1466
|
+
if (seen.has(key)) return;
|
|
1467
|
+
seen.add(key);
|
|
1468
|
+
out.push(normalized);
|
|
1469
|
+
};
|
|
1470
|
+
if (raw == null || raw === "") return out;
|
|
1471
|
+
if (Array.isArray(raw)) {
|
|
1472
|
+
const isFlatDateList = raw.every((item) => (
|
|
1473
|
+
item == null
|
|
1474
|
+
|| typeof item === "string"
|
|
1475
|
+
|| typeof item === "number"
|
|
1476
|
+
|| item instanceof Date
|
|
1477
|
+
));
|
|
1478
|
+
if (isFlatDateList) {
|
|
1479
|
+
const dates = raw.map(normalizeDateOnly).filter(Boolean);
|
|
1480
|
+
for (let index = 0; index < dates.length; index += 2) {
|
|
1481
|
+
pushRange({ from: dates[index], to: dates[index + 1] || null });
|
|
1482
|
+
}
|
|
1483
|
+
} else {
|
|
1484
|
+
raw.forEach((item) => pushRange(item));
|
|
1485
|
+
}
|
|
1486
|
+
} else if (typeof raw === "object" && Array.isArray(raw.ranges)) {
|
|
1487
|
+
raw.ranges.forEach((item) => pushRange(item));
|
|
1488
|
+
} else if (typeof raw === "object") {
|
|
1489
|
+
pushRange(raw);
|
|
1490
|
+
} else {
|
|
1491
|
+
const found = String(raw).match(/\d{4}-\d{1,2}-\d{1,2}|\d{1,2}[\/.\-]\d{1,2}[\/.\-]\d{2,4}/g) || [];
|
|
1492
|
+
const dates = found.map(normalizeDateOnly).filter(Boolean);
|
|
1493
|
+
for (let index = 0; index < dates.length; index += 2) {
|
|
1494
|
+
pushRange({ from: dates[index], to: dates[index + 1] || null });
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return out.sort((a, b) => compareIso(a.from, b.from) || compareIso(a.to, b.to));
|
|
1498
|
+
};
|
|
1499
|
+
const normalizeMultipleValue = (raw) => {
|
|
1500
|
+
const out = [];
|
|
1501
|
+
ensureArray(raw).forEach((item) => {
|
|
1502
|
+
const normalized = normalizeDateOnly(item);
|
|
1503
|
+
if (normalized && !out.includes(normalized)) out.push(normalized);
|
|
1504
|
+
});
|
|
1505
|
+
return out.sort();
|
|
1506
|
+
};
|
|
1507
|
+
const normalizeValue = (raw) => {
|
|
1508
|
+
if (mode === "range") return normalizeRangeValue(raw);
|
|
1509
|
+
if (mode === "range-multiple") return normalizeRangeMultipleValue(raw);
|
|
1510
|
+
if (mode === "multiple") return normalizeMultipleValue(raw);
|
|
1511
|
+
return normalizeDateOnly(raw);
|
|
1512
|
+
};
|
|
1513
|
+
const getMin = () => normalizeDateOnly(uiUnwrap(props.min ?? props.minDate));
|
|
1514
|
+
const getMax = () => normalizeDateOnly(uiUnwrap(props.max ?? props.maxDate));
|
|
1515
|
+
const getLocale = () => uiUnwrap(props.locale) || undefined;
|
|
1516
|
+
const getFirstDayOfWeek = () => {
|
|
1517
|
+
const raw = Number(uiUnwrap(props.firstDayOfWeek ?? props.weekStart ?? 1));
|
|
1518
|
+
return Number.isFinite(raw) ? ((raw % 7) + 7) % 7 : 1;
|
|
1519
|
+
};
|
|
1520
|
+
const getMonthsToShow = () => {
|
|
1521
|
+
const fallback = (mode === "range" || mode === "range-multiple") ? 2 : 1;
|
|
1522
|
+
const raw = Number(uiUnwrap(props.monthsToShow) ?? fallback);
|
|
1523
|
+
return Math.max(1, Math.min(4, Number.isFinite(raw) ? raw : fallback));
|
|
1524
|
+
};
|
|
1525
|
+
const getDefaultViewValue = () => {
|
|
1526
|
+
const min = getMin();
|
|
1527
|
+
const max = getMax();
|
|
1528
|
+
let fallback = normalizeDateOnly(uiUnwrap(props.defaultMonth)) || todayIso();
|
|
1529
|
+
if (min && fallback < min) fallback = min;
|
|
1530
|
+
if (max && fallback > max) fallback = max;
|
|
1531
|
+
return fallback || min || max || todayIso();
|
|
1532
|
+
};
|
|
1533
|
+
const getCurrentValue = () => uiUnwrap(props.confirm) ? workingValue : localValue;
|
|
1534
|
+
const getDateContext = (iso) => {
|
|
1535
|
+
const date = fromIsoDate(iso);
|
|
1536
|
+
if (!date) return { date: iso, value: iso };
|
|
1537
|
+
return {
|
|
1538
|
+
date: iso,
|
|
1539
|
+
value: iso,
|
|
1540
|
+
year: date.getFullYear(),
|
|
1541
|
+
month: date.getMonth() + 1,
|
|
1542
|
+
day: date.getDate(),
|
|
1543
|
+
weekday: date.getDay(),
|
|
1544
|
+
today: isSameIso(iso, todayIso())
|
|
1545
|
+
};
|
|
1546
|
+
};
|
|
1547
|
+
const matchesDateSpec = (spec, iso) => {
|
|
1548
|
+
if (spec == null) return false;
|
|
1549
|
+
if (Array.isArray(spec)) return spec.some((item) => normalizeDateOnly(item) === iso);
|
|
1550
|
+
if (spec instanceof Set) return spec.has(iso);
|
|
1551
|
+
if (typeof spec === "function") return !!spec(iso, getDateContext(iso));
|
|
1552
|
+
if (typeof spec === "string" || spec instanceof Date || typeof spec === "number") {
|
|
1553
|
+
return normalizeDateOnly(spec) === iso;
|
|
1554
|
+
}
|
|
1555
|
+
return false;
|
|
1556
|
+
};
|
|
1557
|
+
const isDateAllowed = (iso) => {
|
|
1558
|
+
if (!iso) return false;
|
|
1559
|
+
const min = getMin();
|
|
1560
|
+
const max = getMax();
|
|
1561
|
+
if (min && iso < min) return false;
|
|
1562
|
+
if (max && iso > max) return false;
|
|
1563
|
+
const allowSpec = uiUnwrap(props.options ?? props.enableDates ?? props.allowedDates);
|
|
1564
|
+
if (allowSpec != null && !matchesDateSpec(allowSpec, iso)) return false;
|
|
1565
|
+
const disableSpec = uiUnwrap(props.disableDates ?? props.disabledDates ?? props.notAllowedDates);
|
|
1566
|
+
if (disableSpec != null && matchesDateSpec(disableSpec, iso)) return false;
|
|
1567
|
+
const weekday = fromIsoDate(iso)?.getDay();
|
|
1568
|
+
if (Array.isArray(uiUnwrap(props.allowedWeekdays ?? props.weekdays)) && weekday != null) {
|
|
1569
|
+
const allowedWeekdays = uiUnwrap(props.allowedWeekdays ?? props.weekdays).map((x) => Number(x));
|
|
1570
|
+
if (!allowedWeekdays.includes(weekday)) return false;
|
|
1571
|
+
}
|
|
1572
|
+
if (uiUnwrap(props.weekdaysOnly) && (weekday === 0 || weekday === 6)) return false;
|
|
1573
|
+
if (uiUnwrap(props.weekendsOnly) && weekday != null && weekday !== 0 && weekday !== 6) return false;
|
|
1574
|
+
const current = getCurrentValue();
|
|
1575
|
+
const pendingRange = mode === "range"
|
|
1576
|
+
? (current?.from && !current?.to ? current : null)
|
|
1577
|
+
: (mode === "range-multiple"
|
|
1578
|
+
? ((Array.isArray(current) && current.length && current[current.length - 1]?.from && !current[current.length - 1]?.to)
|
|
1579
|
+
? current[current.length - 1]
|
|
1580
|
+
: null)
|
|
1581
|
+
: null);
|
|
1582
|
+
if (pendingRange?.from) {
|
|
1583
|
+
const nights = Math.abs(diffDays(pendingRange.from, iso));
|
|
1584
|
+
const minRange = Number(uiUnwrap(props.minRange ?? props.minDays ?? props.minNights));
|
|
1585
|
+
const maxRange = Number(uiUnwrap(props.maxRange ?? props.maxDays ?? props.maxNights));
|
|
1586
|
+
if (Number.isFinite(minRange) && nights < minRange) return false;
|
|
1587
|
+
if (Number.isFinite(maxRange) && nights > maxRange) return false;
|
|
1588
|
+
}
|
|
1589
|
+
return true;
|
|
1590
|
+
};
|
|
1591
|
+
const formatSingleDisplay = (iso) => {
|
|
1592
|
+
if (!iso) return "";
|
|
1593
|
+
const date = fromIsoDate(iso);
|
|
1594
|
+
if (!date) return iso;
|
|
1595
|
+
const displayMask = String(uiUnwrap(props.displayMask ?? props.mask ?? "") || "").toLowerCase();
|
|
1596
|
+
if (displayMask === "iso" || uiUnwrap(props.isoDisplay) === true) return iso;
|
|
1597
|
+
return new Intl.DateTimeFormat(getLocale(), {
|
|
1598
|
+
year: "numeric",
|
|
1599
|
+
month: "short",
|
|
1600
|
+
day: "2-digit"
|
|
1601
|
+
}).format(date);
|
|
1602
|
+
};
|
|
1603
|
+
const formatDisplayValue = (value, timeValue = localTimeValue) => {
|
|
1604
|
+
if (mode === "range-multiple") {
|
|
1605
|
+
if (!Array.isArray(value) || !value.length) return "";
|
|
1606
|
+
if (value.length > 2 && uiUnwrap(props.compactMultiple) !== false) {
|
|
1607
|
+
return `${value.length} ${uiUnwrap(props.multipleRangeLabel) || "intervalli selezionati"}`;
|
|
1608
|
+
}
|
|
1609
|
+
return value.map((range) => {
|
|
1610
|
+
const from = range?.from ? formatSingleDisplay(range.from) : "";
|
|
1611
|
+
const to = range?.to ? formatSingleDisplay(range.to) : "";
|
|
1612
|
+
return from && to ? `${from} -> ${to}` : (from || to || "");
|
|
1613
|
+
}).filter(Boolean).join("; ");
|
|
1614
|
+
}
|
|
1615
|
+
if (mode === "multiple") {
|
|
1616
|
+
if (!Array.isArray(value) || !value.length) return "";
|
|
1617
|
+
if (value.length > 3 && uiUnwrap(props.compactMultiple) !== false) {
|
|
1618
|
+
return `${value.length} ${uiUnwrap(props.multipleLabel) || "date selezionate"}`;
|
|
1619
|
+
}
|
|
1620
|
+
return value.map(formatSingleDisplay).join(", ");
|
|
1621
|
+
}
|
|
1622
|
+
if (mode === "range") {
|
|
1623
|
+
const from = value?.from ? formatSingleDisplay(value.from) : "";
|
|
1624
|
+
const to = value?.to ? formatSingleDisplay(value.to) : "";
|
|
1625
|
+
if (from && to) return `${from} -> ${to}`;
|
|
1626
|
+
return from || to || "";
|
|
1627
|
+
}
|
|
1628
|
+
const dateLabel = formatSingleDisplay(value);
|
|
1629
|
+
if (!isTimeEnabled()) return dateLabel;
|
|
1630
|
+
const timeLabel = formatTimeDisplayValue(timeValue);
|
|
1631
|
+
return [dateLabel, timeLabel].filter(Boolean).join(" • ");
|
|
1632
|
+
};
|
|
1633
|
+
const extractTypedDates = (raw) => {
|
|
1634
|
+
const found = String(raw || "").match(/\d{4}-\d{1,2}-\d{1,2}|\d{1,2}[\/.\-]\d{1,2}[\/.\-]\d{2,4}/g) || [];
|
|
1635
|
+
return found.map(normalizeDateOnly).filter(Boolean);
|
|
1636
|
+
};
|
|
1637
|
+
const parseTypedValue = (raw) => {
|
|
1638
|
+
if (raw == null || raw === "") {
|
|
1639
|
+
if (mode === "multiple" || mode === "range-multiple") return [];
|
|
1640
|
+
if (mode === "range") return { from: null, to: null };
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
const dates = extractTypedDates(raw);
|
|
1644
|
+
if (mode === "range-multiple") {
|
|
1645
|
+
const ranges = [];
|
|
1646
|
+
for (let index = 0; index < dates.length; index += 2) {
|
|
1647
|
+
if (!dates[index]) continue;
|
|
1648
|
+
ranges.push({ from: dates[index], to: dates[index + 1] || null });
|
|
1649
|
+
}
|
|
1650
|
+
return ranges;
|
|
1651
|
+
}
|
|
1652
|
+
if (mode === "multiple") return dates;
|
|
1653
|
+
if (mode === "range") return { from: dates[0] || null, to: dates[1] || null };
|
|
1654
|
+
return dates[0] || normalizeDateOnly(raw);
|
|
1655
|
+
};
|
|
1656
|
+
const emptyValue = () => ((mode === "multiple" || mode === "range-multiple") ? [] : (mode === "range" ? { from: null, to: null } : null));
|
|
1657
|
+
const resolveDateTimeTimeValue = (raw, fallback = null) => {
|
|
1658
|
+
if (!isTimeEnabled()) return null;
|
|
1659
|
+
const options = getTimeOptions();
|
|
1660
|
+
const explicit = uiUnwrap(props.timeValue);
|
|
1661
|
+
const source = raw != null ? raw : explicit;
|
|
1662
|
+
const resolved = uiExtractTimeFromValue(source, options);
|
|
1663
|
+
return uiCloneTimeParts(resolved ?? fallback ?? null);
|
|
1664
|
+
};
|
|
1665
|
+
const getCurrentTimeValue = () => uiUnwrap(props.confirm) ? workingTimeValue : localTimeValue;
|
|
1666
|
+
const exportCurrentValue = (dateValue = localValue, timeValue = localTimeValue) => {
|
|
1667
|
+
if (mode === "single" && isTimeEnabled()) return uiMergeDateAndTime(dateValue || null, timeValue, getTimeOptions());
|
|
1668
|
+
return exportValue(dateValue);
|
|
1669
|
+
};
|
|
1670
|
+
const serializeCurrentValue = (dateValue = localValue, timeValue = localTimeValue) => JSON.stringify(exportCurrentValue(dateValue, timeValue));
|
|
1671
|
+
const formatTimeDisplayValue = (value) => uiFormatTimeDisplay(value, getTimeOptions());
|
|
1672
|
+
|
|
1673
|
+
let localValue = normalizeValue(initialRawValue);
|
|
1674
|
+
let workingValue = cloneValue(localValue);
|
|
1675
|
+
let localTimeValue = resolveDateTimeTimeValue(initialRawValue, uiExtractTimeFromValue(uiUnwrap(props.timeValue), getTimeOptions()));
|
|
1676
|
+
let workingTimeValue = uiCloneTimeParts(localTimeValue);
|
|
1677
|
+
let hoverDate = null;
|
|
1678
|
+
let mouseSelectedDate = null;
|
|
1679
|
+
let viewMonth = monthStart(
|
|
1680
|
+
mode === "range"
|
|
1681
|
+
? (localValue?.from || localValue?.to || getDefaultViewValue())
|
|
1682
|
+
: (mode === "range-multiple"
|
|
1683
|
+
? (localValue?.[localValue.length - 1]?.from || localValue?.[localValue.length - 1]?.to || getDefaultViewValue())
|
|
1684
|
+
: (mode === "multiple"
|
|
1685
|
+
? (localValue[0] || getDefaultViewValue())
|
|
1686
|
+
: (localValue || getDefaultViewValue())))
|
|
1687
|
+
);
|
|
1688
|
+
let entry = null;
|
|
1689
|
+
let panelRoot = null;
|
|
1690
|
+
|
|
1691
|
+
const getVisibleMonthOffset = (value) => {
|
|
1692
|
+
const date = value instanceof Date ? value : fromIsoDate(normalizeDateOnly(value));
|
|
1693
|
+
if (!date) return -1;
|
|
1694
|
+
const year = date.getFullYear();
|
|
1695
|
+
const month = date.getMonth();
|
|
1696
|
+
for (let index = 0; index < getMonthsToShow(); index += 1) {
|
|
1697
|
+
const visibleMonth = shiftMonths(viewMonth, index);
|
|
1698
|
+
if (visibleMonth.getFullYear() === year && visibleMonth.getMonth() === month) return index;
|
|
1699
|
+
}
|
|
1700
|
+
return -1;
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
const fieldProps = {
|
|
1704
|
+
...props,
|
|
1705
|
+
label: props.label ?? props.placeholder
|
|
1706
|
+
};
|
|
1707
|
+
const hasFloatingLabel = fieldProps.label != null || JSswift.ui.getSlot(slots, "label") != null;
|
|
1708
|
+
const defaultPlaceholder = mode === "range"
|
|
1709
|
+
? "Seleziona andata e ritorno"
|
|
1710
|
+
: (mode === "range-multiple"
|
|
1711
|
+
? "Seleziona intervalli"
|
|
1712
|
+
: (mode === "multiple" ? "Seleziona date" : "Seleziona data"));
|
|
1713
|
+
|
|
1714
|
+
const displayInput = isCalendar ? null : _.input({
|
|
1715
|
+
class: uiClass(["jss-input", "jss-date-display", sizeValue, uiWhen(props.manualInput, "is-manual"), props.inputClass]),
|
|
1716
|
+
type: "text",
|
|
1717
|
+
autocomplete: "off",
|
|
1718
|
+
placeholder: hasFloatingLabel ? "" : (props.placeholder ?? defaultPlaceholder),
|
|
1719
|
+
readOnly: !uiUnwrap(props.manualInput),
|
|
1720
|
+
disabled: !!uiUnwrap(props.disabled),
|
|
1721
|
+
value: formatDisplayValue(localValue, localTimeValue)
|
|
1722
|
+
});
|
|
1723
|
+
const hiddenHost = _.div({ style: { display: "contents" } });
|
|
1724
|
+
const controlNode = isCalendar ? null : _.div({ class: "jss-date-control", style: { display: "contents" } }, displayInput, hiddenHost);
|
|
1725
|
+
|
|
1726
|
+
const field = isCalendar ? null : UI.FormField({
|
|
1727
|
+
...fieldProps,
|
|
1728
|
+
iconRight: props.iconRight ?? "calendar_month",
|
|
1729
|
+
control: controlNode,
|
|
1730
|
+
getValue: () => displayInput.value,
|
|
1731
|
+
onClear: () => {
|
|
1732
|
+
if (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return;
|
|
1733
|
+
setDateValue(emptyValue(), null);
|
|
1734
|
+
if (entry) closePanel();
|
|
1735
|
+
},
|
|
1736
|
+
onFocus: () => displayInput.focus()
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
const syncViewMonth = (value, options = {}) => {
|
|
1740
|
+
const current = options.focusIso || (
|
|
1741
|
+
mode === "range"
|
|
1742
|
+
? (value?.from || value?.to || getDefaultViewValue())
|
|
1743
|
+
: (mode === "range-multiple"
|
|
1744
|
+
? (value?.[value.length - 1]?.from || value?.[value.length - 1]?.to || getDefaultViewValue())
|
|
1745
|
+
: (mode === "multiple"
|
|
1746
|
+
? (value?.[0] || getDefaultViewValue())
|
|
1747
|
+
: (value || getDefaultViewValue())))
|
|
1748
|
+
);
|
|
1749
|
+
let nextViewMonth = monthStart(current);
|
|
1750
|
+
if (options.preserveMonthOffset) {
|
|
1751
|
+
const monthOffset = Number.isInteger(options.visibleMonthOffset)
|
|
1752
|
+
? options.visibleMonthOffset
|
|
1753
|
+
: getVisibleMonthOffset(options.anchorIso || options.focusIso || current);
|
|
1754
|
+
if (monthOffset > 0) nextViewMonth = shiftMonths(nextViewMonth, -monthOffset);
|
|
1755
|
+
}
|
|
1756
|
+
viewMonth = nextViewMonth;
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
const syncHiddenInputs = () => {
|
|
1760
|
+
hiddenHost.replaceChildren();
|
|
1761
|
+
const baseName = uiUnwrap(props.name);
|
|
1762
|
+
if (!baseName) return;
|
|
1763
|
+
const appendHidden = (name, value) => {
|
|
1764
|
+
hiddenHost.appendChild(_.input({ type: "hidden", name, value: value ?? "" }));
|
|
1765
|
+
};
|
|
1766
|
+
const value = localValue;
|
|
1767
|
+
if (mode === "single") {
|
|
1768
|
+
appendHidden(baseName, exportCurrentValue(value, localTimeValue));
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (mode === "range") {
|
|
1772
|
+
appendHidden(uiUnwrap(props.nameFrom) || `${baseName}_from`, value?.from || "");
|
|
1773
|
+
appendHidden(uiUnwrap(props.nameTo) || `${baseName}_to`, value?.to || "");
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
if (mode === "range-multiple") {
|
|
1777
|
+
const rangeList = Array.isArray(value) ? value : [];
|
|
1778
|
+
const fromName = /\[\]$/.test(uiUnwrap(props.nameFrom) || "") ? uiUnwrap(props.nameFrom) : `${uiUnwrap(props.nameFrom) || `${baseName}_from`}[]`;
|
|
1779
|
+
const toName = /\[\]$/.test(uiUnwrap(props.nameTo) || "") ? uiUnwrap(props.nameTo) : `${uiUnwrap(props.nameTo) || `${baseName}_to`}[]`;
|
|
1780
|
+
rangeList.forEach((item) => {
|
|
1781
|
+
appendHidden(fromName, item?.from || "");
|
|
1782
|
+
appendHidden(toName, item?.to || "");
|
|
1783
|
+
});
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
const listName = /\[\]$/.test(baseName) ? baseName : `${baseName}[]`;
|
|
1787
|
+
(Array.isArray(value) ? value : []).forEach((item) => appendHidden(listName, item));
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
const syncDisplay = () => {
|
|
1791
|
+
if (isCalendar) return;
|
|
1792
|
+
displayInput.readOnly = !uiUnwrap(props.manualInput);
|
|
1793
|
+
displayInput.disabled = !!uiUnwrap(props.disabled);
|
|
1794
|
+
displayInput.setAttribute("aria-expanded", entry ? "true" : "false");
|
|
1795
|
+
displayInput.value = formatDisplayValue(localValue, localTimeValue);
|
|
1796
|
+
syncHiddenInputs();
|
|
1797
|
+
field?._refresh?.();
|
|
1798
|
+
};
|
|
1799
|
+
|
|
1800
|
+
const setDateValue = (nextValue, event, options = {}) => {
|
|
1801
|
+
const normalized = normalizeValue(nextValue);
|
|
1802
|
+
const nextTime = isTimeEnabled()
|
|
1803
|
+
? uiCloneTimeParts(
|
|
1804
|
+
uiExtractTimeFromValue(
|
|
1805
|
+
options.timeValue != null
|
|
1806
|
+
? options.timeValue
|
|
1807
|
+
: (options.preserveTime ? getCurrentTimeValue() : nextValue),
|
|
1808
|
+
getTimeOptions()
|
|
1809
|
+
) ?? (options.preserveTime ? getCurrentTimeValue() : null)
|
|
1810
|
+
)
|
|
1811
|
+
: null;
|
|
1812
|
+
const prev = serializeCurrentValue(localValue, localTimeValue);
|
|
1813
|
+
localValue = cloneValue(normalized);
|
|
1814
|
+
workingValue = cloneValue(normalized);
|
|
1815
|
+
if (isTimeEnabled()) {
|
|
1816
|
+
localTimeValue = uiCloneTimeParts(nextTime);
|
|
1817
|
+
workingTimeValue = uiCloneTimeParts(nextTime);
|
|
1818
|
+
}
|
|
1819
|
+
hoverDate = null;
|
|
1820
|
+
syncViewMonth(normalized, options);
|
|
1821
|
+
syncDisplay();
|
|
1822
|
+
renderPanel();
|
|
1823
|
+
if (model && options.fromModel !== true) model.set(exportCurrentValue(normalized, nextTime));
|
|
1824
|
+
const nextSerialized = serializeCurrentValue(normalized, nextTime);
|
|
1825
|
+
if (options.emit !== false && nextSerialized !== prev) {
|
|
1826
|
+
const emitted = exportCurrentValue(normalized, nextTime);
|
|
1827
|
+
props.onInput?.(emitted, event);
|
|
1828
|
+
props.onChange?.(emitted, event);
|
|
1829
|
+
}
|
|
1830
|
+
return normalized;
|
|
1831
|
+
};
|
|
1832
|
+
const setTimeValue = (nextValue, event, options = {}) => {
|
|
1833
|
+
if (!isTimeEnabled()) return null;
|
|
1834
|
+
const normalizedTime = uiCloneTimeParts(uiExtractTimeFromValue(nextValue, getTimeOptions()));
|
|
1835
|
+
if (uiUnwrap(props.confirm) && options.commit !== true) {
|
|
1836
|
+
workingTimeValue = normalizedTime;
|
|
1837
|
+
renderPanel();
|
|
1838
|
+
return normalizedTime;
|
|
1839
|
+
}
|
|
1840
|
+
const prev = serializeCurrentValue(localValue, localTimeValue);
|
|
1841
|
+
localTimeValue = uiCloneTimeParts(normalizedTime);
|
|
1842
|
+
workingTimeValue = uiCloneTimeParts(normalizedTime);
|
|
1843
|
+
syncDisplay();
|
|
1844
|
+
renderPanel();
|
|
1845
|
+
if (model && options.fromModel !== true) model.set(exportCurrentValue(localValue, normalizedTime));
|
|
1846
|
+
const nextSerialized = serializeCurrentValue(localValue, normalizedTime);
|
|
1847
|
+
if (options.emit !== false && nextSerialized !== prev) {
|
|
1848
|
+
const emitted = exportCurrentValue(localValue, normalizedTime);
|
|
1849
|
+
props.onInput?.(emitted, event);
|
|
1850
|
+
props.onChange?.(emitted, event);
|
|
1851
|
+
}
|
|
1852
|
+
return normalizedTime;
|
|
1853
|
+
};
|
|
1854
|
+
const setWorkingOrCommit = (nextValue, event, options = {}) => {
|
|
1855
|
+
if (uiUnwrap(props.confirm)) {
|
|
1856
|
+
workingValue = cloneValue(normalizeValue(nextValue));
|
|
1857
|
+
if (isTimeEnabled() && (options.timeValue != null || options.preserveTime)) {
|
|
1858
|
+
workingTimeValue = uiCloneTimeParts(uiExtractTimeFromValue(
|
|
1859
|
+
options.timeValue != null ? options.timeValue : getCurrentTimeValue(),
|
|
1860
|
+
getTimeOptions()
|
|
1861
|
+
));
|
|
1862
|
+
}
|
|
1863
|
+
hoverDate = null;
|
|
1864
|
+
syncViewMonth(workingValue, options);
|
|
1865
|
+
renderPanel();
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
setDateValue(nextValue, event, options);
|
|
1869
|
+
};
|
|
1870
|
+
const selectionPreview = () => {
|
|
1871
|
+
const current = getCurrentValue();
|
|
1872
|
+
if (mode === "range") {
|
|
1873
|
+
if (!current?.from || current?.to || !hoverDate) return current;
|
|
1874
|
+
return compareIso(current.from, hoverDate) <= 0
|
|
1875
|
+
? { from: current.from, to: hoverDate }
|
|
1876
|
+
: { from: hoverDate, to: current.from };
|
|
1877
|
+
}
|
|
1878
|
+
if (mode === "range-multiple") {
|
|
1879
|
+
const last = Array.isArray(current) && current.length ? current[current.length - 1] : null;
|
|
1880
|
+
if (!last?.from || last?.to || !hoverDate) return last;
|
|
1881
|
+
return compareIso(last.from, hoverDate) <= 0
|
|
1882
|
+
? { from: last.from, to: hoverDate }
|
|
1883
|
+
: { from: hoverDate, to: last.from };
|
|
1884
|
+
}
|
|
1885
|
+
return current;
|
|
1886
|
+
};
|
|
1887
|
+
const renderedRanges = () => {
|
|
1888
|
+
if (mode === "range") {
|
|
1889
|
+
const preview = selectionPreview();
|
|
1890
|
+
return preview?.from ? [preview] : [];
|
|
1891
|
+
}
|
|
1892
|
+
if (mode === "range-multiple") {
|
|
1893
|
+
const current = Array.isArray(getCurrentValue()) ? getCurrentValue() : [];
|
|
1894
|
+
if (!current.length) return [];
|
|
1895
|
+
const preview = selectionPreview();
|
|
1896
|
+
if (!preview?.from) return current;
|
|
1897
|
+
return [...current.slice(0, -1), preview];
|
|
1898
|
+
}
|
|
1899
|
+
return [];
|
|
1900
|
+
};
|
|
1901
|
+
const isSelectedDate = (iso) => {
|
|
1902
|
+
const current = getCurrentValue();
|
|
1903
|
+
if (mode === "multiple") return current.includes(iso);
|
|
1904
|
+
if (mode === "range-multiple") {
|
|
1905
|
+
return Array.isArray(current) && current.some((range) => isSameIso(range?.from, iso) || isSameIso(range?.to, iso));
|
|
1906
|
+
}
|
|
1907
|
+
if (mode === "range") return isSameIso(current?.from, iso) || isSameIso(current?.to, iso);
|
|
1908
|
+
return isSameIso(current, iso);
|
|
1909
|
+
};
|
|
1910
|
+
const isInRange = (iso) => {
|
|
1911
|
+
if (mode !== "range" && mode !== "range-multiple") return false;
|
|
1912
|
+
return renderedRanges().some((range) => range?.from && range?.to && iso >= range.from && iso <= range.to);
|
|
1913
|
+
};
|
|
1914
|
+
const updateRangeHover = (iso, options = {}) => {
|
|
1915
|
+
const current = getCurrentValue();
|
|
1916
|
+
const pendingRange = mode === "range"
|
|
1917
|
+
? current
|
|
1918
|
+
: (mode === "range-multiple"
|
|
1919
|
+
? (Array.isArray(current) && current.length ? current[current.length - 1] : null)
|
|
1920
|
+
: null);
|
|
1921
|
+
if (!pendingRange?.from || pendingRange?.to || hoverDate === iso) return;
|
|
1922
|
+
hoverDate = iso;
|
|
1923
|
+
// Re-rendering on focus replaces the clicked day button before the click event fires.
|
|
1924
|
+
if (options.render !== false) renderPanel();
|
|
1925
|
+
};
|
|
1926
|
+
const shouldCloseOnSelect = () => {
|
|
1927
|
+
if (isCalendar) return false;
|
|
1928
|
+
if (uiUnwrap(props.confirm)) return false;
|
|
1929
|
+
if (props.closeOnSelect === false) return false;
|
|
1930
|
+
if (mode === "multiple" || mode === "range-multiple") return !!props.closeOnSelect;
|
|
1931
|
+
return true;
|
|
1932
|
+
};
|
|
1933
|
+
const selectDate = (iso, event, selectionOptions = {}) => {
|
|
1934
|
+
if (!isDateAllowed(iso) || uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return;
|
|
1935
|
+
const syncOptions = {
|
|
1936
|
+
focusIso: iso,
|
|
1937
|
+
anchorIso: iso,
|
|
1938
|
+
preserveTime: isTimeEnabled(),
|
|
1939
|
+
timeValue: getCurrentTimeValue(),
|
|
1940
|
+
preserveMonthOffset: !!entry && getMonthsToShow() > 1,
|
|
1941
|
+
visibleMonthOffset: Number.isInteger(selectionOptions.visibleMonthOffset) ? selectionOptions.visibleMonthOffset : null
|
|
1942
|
+
};
|
|
1943
|
+
if (mode === "multiple") {
|
|
1944
|
+
const list = normalizeMultipleValue(getCurrentValue());
|
|
1945
|
+
const next = list.includes(iso) ? list.filter((item) => item !== iso) : [...list, iso];
|
|
1946
|
+
setWorkingOrCommit(next.sort(), event, syncOptions);
|
|
1947
|
+
if (shouldCloseOnSelect()) closePanel();
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
if (mode === "range") {
|
|
1951
|
+
const current = cloneValue(getCurrentValue());
|
|
1952
|
+
let next;
|
|
1953
|
+
if (!current.from || (current.from && current.to)) {
|
|
1954
|
+
next = { from: iso, to: null };
|
|
1955
|
+
} else if (compareIso(iso, current.from) < 0) {
|
|
1956
|
+
next = { from: iso, to: current.from };
|
|
1957
|
+
} else {
|
|
1958
|
+
next = { from: current.from, to: iso };
|
|
1959
|
+
}
|
|
1960
|
+
setWorkingOrCommit(next, event, syncOptions);
|
|
1961
|
+
if (next.to && shouldCloseOnSelect()) closePanel();
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (mode === "range-multiple") {
|
|
1965
|
+
const list = normalizeRangeMultipleValue(getCurrentValue());
|
|
1966
|
+
const last = list.length ? list[list.length - 1] : null;
|
|
1967
|
+
let next;
|
|
1968
|
+
if (!last?.from || last?.to) {
|
|
1969
|
+
next = [...list, { from: iso, to: null }];
|
|
1970
|
+
} else if (compareIso(iso, last.from) < 0) {
|
|
1971
|
+
next = [...list.slice(0, -1), { from: iso, to: last.from }];
|
|
1972
|
+
} else {
|
|
1973
|
+
next = [...list.slice(0, -1), { from: last.from, to: iso }];
|
|
1974
|
+
}
|
|
1975
|
+
setWorkingOrCommit(next, event, syncOptions);
|
|
1976
|
+
if (next[next.length - 1]?.to && shouldCloseOnSelect()) closePanel();
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
setWorkingOrCommit(iso, event, syncOptions);
|
|
1980
|
+
if (shouldCloseOnSelect()) closePanel();
|
|
1981
|
+
};
|
|
1982
|
+
const clearValue = () => {
|
|
1983
|
+
mouseSelectedDate = null;
|
|
1984
|
+
setDateValue(emptyValue(), null, { timeValue: null });
|
|
1985
|
+
};
|
|
1986
|
+
const jumpToToday = () => {
|
|
1987
|
+
const today = todayIso();
|
|
1988
|
+
if (!isDateAllowed(today)) {
|
|
1989
|
+
syncViewMonth(today);
|
|
1990
|
+
renderPanel();
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
if (mode === "range") {
|
|
1994
|
+
const tomorrow = shiftDays(today, 1);
|
|
1995
|
+
const next = tomorrow && isDateAllowed(tomorrow) ? { from: today, to: tomorrow } : { from: today, to: null };
|
|
1996
|
+
setWorkingOrCommit(next, null);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
if (mode === "range-multiple") {
|
|
2000
|
+
const tomorrow = shiftDays(today, 1);
|
|
2001
|
+
const next = tomorrow && isDateAllowed(tomorrow)
|
|
2002
|
+
? [{ from: today, to: tomorrow }]
|
|
2003
|
+
: [{ from: today, to: null }];
|
|
2004
|
+
setWorkingOrCommit(next, null);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
if (mode === "multiple") {
|
|
2008
|
+
setWorkingOrCommit([today], null);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
setWorkingOrCommit(today, null, { preserveTime: isTimeEnabled(), timeValue: getCurrentTimeValue() });
|
|
2012
|
+
};
|
|
2013
|
+
const scrollTimeColumnsToSelection = (options = {}) => {
|
|
2014
|
+
uiCenterTimeColumnsToSelection(panelRoot, options);
|
|
2015
|
+
};
|
|
2016
|
+
const jumpToNow = () => {
|
|
2017
|
+
if (!isTimeEnabled()) return;
|
|
2018
|
+
const scrollState = uiCaptureTimeColumnScrollState(panelRoot);
|
|
2019
|
+
setTimeValue(new Date(), null, { commit: !uiUnwrap(props.confirm) });
|
|
2020
|
+
scrollTimeColumnsToSelection({ scrollState });
|
|
2021
|
+
if (!uiUnwrap(props.confirm) && props.closeOnSelect === true) closePanel();
|
|
2022
|
+
};
|
|
2023
|
+
const applyWorkingValue = () => {
|
|
2024
|
+
if (!uiUnwrap(props.confirm)) return;
|
|
2025
|
+
setDateValue(workingValue, null, { timeValue: workingTimeValue, preserveTime: true });
|
|
2026
|
+
if (props.closeOnSelect !== false) closePanel();
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
const renderPanel = () => {
|
|
2030
|
+
if (!panelRoot) return;
|
|
2031
|
+
const locale = getLocale();
|
|
2032
|
+
const monthLabelFormatter = new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" });
|
|
2033
|
+
const weekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
|
|
2034
|
+
const titleDates = Array.from({ length: 7 }, (_, index) => {
|
|
2035
|
+
const day = (getFirstDayOfWeek() + index) % 7;
|
|
2036
|
+
return weekdayFormatter.format(new Date(2024, 0, 7 + day));
|
|
2037
|
+
});
|
|
2038
|
+
const pickerValue = getCurrentValue();
|
|
2039
|
+
|
|
2040
|
+
const buildDayPoint = (ctx) => {
|
|
2041
|
+
let pointNode = JSswift.ui.renderSlot(slots, "point", ctx, null);
|
|
2042
|
+
if (pointNode == null) pointNode = JSswift.ui.renderSlot(slots, "dayPoint", ctx, null);
|
|
2043
|
+
if (pointNode == null && props.pointIcon != null && (ctx.selected || ctx.inRange || ctx.today)) {
|
|
2044
|
+
pointNode = typeof props.pointIcon === "string"
|
|
2045
|
+
? UI.Icon({ name: props.pointIcon, size: 10 })
|
|
2046
|
+
: JSswift.ui.slot(props.pointIcon, ctx);
|
|
2047
|
+
}
|
|
2048
|
+
return renderSlotToArray(null, "default", ctx, pointNode);
|
|
2049
|
+
};
|
|
2050
|
+
|
|
2051
|
+
const renderMonth = (baseDate, monthOffset) => {
|
|
2052
|
+
const year = baseDate.getFullYear();
|
|
2053
|
+
const month = baseDate.getMonth();
|
|
2054
|
+
const start = new Date(year, month, 1);
|
|
2055
|
+
const offset = (start.getDay() - getFirstDayOfWeek() + 7) % 7;
|
|
2056
|
+
const gridStart = new Date(year, month, 1 - offset);
|
|
2057
|
+
const monthBox = _.div({ class: "jss-date-month" });
|
|
2058
|
+
monthBox.appendChild(_.div({ class: "jss-date-month-title" }, monthLabelFormatter.format(start)));
|
|
2059
|
+
|
|
2060
|
+
const weekdays = _.div({ class: "jss-date-weekdays" });
|
|
2061
|
+
titleDates.forEach((label) => weekdays.appendChild(_.div({ class: "jss-date-weekday" }, label)));
|
|
2062
|
+
monthBox.appendChild(weekdays);
|
|
2063
|
+
|
|
2064
|
+
const grid = _.div({ class: "jss-date-grid" });
|
|
2065
|
+
for (let index = 0; index < 42; index += 1) {
|
|
2066
|
+
const date = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + index);
|
|
2067
|
+
const iso = toIsoDate(date);
|
|
2068
|
+
const inMonth = date.getMonth() === month;
|
|
2069
|
+
const selected = isSelectedDate(iso);
|
|
2070
|
+
const inRange = isInRange(iso);
|
|
2071
|
+
const rangeStart = (mode === "range" || mode === "range-multiple") && renderedRanges().some((range) => isSameIso(range?.from, iso));
|
|
2072
|
+
const rangeEnd = (mode === "range" || mode === "range-multiple") && renderedRanges().some((range) => isSameIso(range?.to, iso));
|
|
2073
|
+
const disabled = !isDateAllowed(iso);
|
|
2074
|
+
const ctx = {
|
|
2075
|
+
...getDateContext(iso),
|
|
2076
|
+
selected,
|
|
2077
|
+
inRange,
|
|
2078
|
+
rangeStart,
|
|
2079
|
+
rangeEnd,
|
|
2080
|
+
disabled,
|
|
2081
|
+
outside: !inMonth,
|
|
2082
|
+
value: pickerValue,
|
|
2083
|
+
select: () => selectDate(iso, null, { visibleMonthOffset: monthOffset })
|
|
2084
|
+
};
|
|
2085
|
+
const pointNodes = buildDayPoint(ctx);
|
|
2086
|
+
const labelNode = JSswift.ui.renderSlot(slots, "day", ctx, String(date.getDate()));
|
|
2087
|
+
const labelNodes = renderSlotToArray(null, "default", ctx, labelNode);
|
|
2088
|
+
const dayBtn = _.button({
|
|
2089
|
+
type: "button",
|
|
2090
|
+
class: uiClass([
|
|
2091
|
+
"jss-date-day",
|
|
2092
|
+
uiWhen(!inMonth, "is-outside"),
|
|
2093
|
+
uiWhen(selected, "is-selected"),
|
|
2094
|
+
uiWhen(inRange, "is-in-range"),
|
|
2095
|
+
uiWhen(rangeStart, "is-range-start"),
|
|
2096
|
+
uiWhen(rangeEnd, "is-range-end"),
|
|
2097
|
+
uiWhen(disabled, "is-disabled"),
|
|
2098
|
+
uiWhen(isSameIso(iso, todayIso()), "is-today"),
|
|
2099
|
+
uiWhen(pointNodes.length, "has-point")
|
|
2100
|
+
]),
|
|
2101
|
+
disabled,
|
|
2102
|
+
onMouseDown: (event) => {
|
|
2103
|
+
if (event.button !== 0) return;
|
|
2104
|
+
mouseSelectedDate = iso;
|
|
2105
|
+
event.preventDefault();
|
|
2106
|
+
selectDate(iso, event, { visibleMonthOffset: monthOffset });
|
|
2107
|
+
},
|
|
2108
|
+
onClick: (event) => {
|
|
2109
|
+
if (mouseSelectedDate != null && event.detail !== 0) {
|
|
2110
|
+
mouseSelectedDate = null;
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
mouseSelectedDate = null;
|
|
2114
|
+
selectDate(iso, event, { visibleMonthOffset: monthOffset });
|
|
2115
|
+
},
|
|
2116
|
+
onMouseEnter: () => updateRangeHover(iso),
|
|
2117
|
+
onFocus: () => updateRangeHover(iso, { render: false })
|
|
2118
|
+
},
|
|
2119
|
+
_.span({ class: "jss-date-day-label" }, ...labelNodes),
|
|
2120
|
+
pointNodes.length ? _.span({ class: "jss-date-day-point" }, ...pointNodes) : null
|
|
2121
|
+
);
|
|
2122
|
+
grid.appendChild(dayBtn);
|
|
2123
|
+
}
|
|
2124
|
+
monthBox.appendChild(grid);
|
|
2125
|
+
return monthBox;
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2128
|
+
const min = getMin();
|
|
2129
|
+
const max = getMax();
|
|
2130
|
+
const candidateYears = [
|
|
2131
|
+
viewMonth.getFullYear(),
|
|
2132
|
+
fromIsoDate(todayIso())?.getFullYear(),
|
|
2133
|
+
fromIsoDate(min)?.getFullYear(),
|
|
2134
|
+
fromIsoDate(max)?.getFullYear(),
|
|
2135
|
+
fromIsoDate(uiUnwrap(props.defaultMonth))?.getFullYear()
|
|
2136
|
+
];
|
|
2137
|
+
if (mode === "range") {
|
|
2138
|
+
candidateYears.push(fromIsoDate(pickerValue?.from)?.getFullYear(), fromIsoDate(pickerValue?.to)?.getFullYear());
|
|
2139
|
+
} else if (mode === "range-multiple") {
|
|
2140
|
+
pickerValue.forEach((item) => {
|
|
2141
|
+
candidateYears.push(fromIsoDate(item?.from)?.getFullYear(), fromIsoDate(item?.to)?.getFullYear());
|
|
2142
|
+
});
|
|
2143
|
+
} else if (mode === "multiple") {
|
|
2144
|
+
pickerValue.forEach((item) => candidateYears.push(fromIsoDate(item)?.getFullYear()));
|
|
2145
|
+
} else {
|
|
2146
|
+
candidateYears.push(fromIsoDate(pickerValue)?.getFullYear());
|
|
2147
|
+
}
|
|
2148
|
+
const validYears = candidateYears.filter((year) => Number.isFinite(year));
|
|
2149
|
+
const inferredYearStart = (validYears.length ? Math.min(...validYears) : viewMonth.getFullYear()) - 100;
|
|
2150
|
+
const inferredYearEnd = (validYears.length ? Math.max(...validYears) : viewMonth.getFullYear()) + 50;
|
|
2151
|
+
const yearStart = Number(uiUnwrap(props.yearStart ?? (min ? fromIsoDate(min)?.getFullYear() : null) ?? inferredYearStart));
|
|
2152
|
+
const yearEnd = Number(uiUnwrap(props.yearEnd ?? (max ? fromIsoDate(max)?.getFullYear() : null) ?? inferredYearEnd));
|
|
2153
|
+
const monthSelect = _.select({
|
|
2154
|
+
class: "jss-date-select",
|
|
2155
|
+
value: String(viewMonth.getMonth()),
|
|
2156
|
+
onChange: (event) => {
|
|
2157
|
+
viewMonth = new Date(viewMonth.getFullYear(), Number(event.currentTarget.value), 1);
|
|
2158
|
+
props.onNavigate?.({ month: viewMonth.getMonth() + 1, year: viewMonth.getFullYear() });
|
|
2159
|
+
renderPanel();
|
|
2160
|
+
}
|
|
2161
|
+
},
|
|
2162
|
+
...Array.from({ length: 12 }, (_, index) => uiOptionNode({ value: String(index) }, new Intl.DateTimeFormat(locale, { month: "long" }).format(new Date(2024, index, 1))))
|
|
2163
|
+
);
|
|
2164
|
+
const yearSelect = _.select({
|
|
2165
|
+
class: "jss-date-select jss-date-select-year",
|
|
2166
|
+
value: String(viewMonth.getFullYear()),
|
|
2167
|
+
onChange: (event) => {
|
|
2168
|
+
viewMonth = new Date(Number(event.currentTarget.value), viewMonth.getMonth(), 1);
|
|
2169
|
+
props.onNavigate?.({ month: viewMonth.getMonth() + 1, year: viewMonth.getFullYear() });
|
|
2170
|
+
renderPanel();
|
|
2171
|
+
}
|
|
2172
|
+
},
|
|
2173
|
+
...Array.from({ length: Math.max(1, yearEnd - yearStart + 1) }, (_, index) => {
|
|
2174
|
+
const year = yearStart + index;
|
|
2175
|
+
return uiOptionNode({ value: String(year) }, String(year));
|
|
2176
|
+
})
|
|
2177
|
+
);
|
|
2178
|
+
|
|
2179
|
+
const header = _.div({ class: "jss-date-header" },
|
|
2180
|
+
_.button({
|
|
2181
|
+
type: "button",
|
|
2182
|
+
class: "jss-date-nav",
|
|
2183
|
+
onClick: () => {
|
|
2184
|
+
viewMonth = shiftMonths(viewMonth, -1);
|
|
2185
|
+
props.onNavigate?.({ month: viewMonth.getMonth() + 1, year: viewMonth.getFullYear() });
|
|
2186
|
+
renderPanel();
|
|
2187
|
+
}
|
|
2188
|
+
}, UI.Icon({ name: "chevron_left", size: 16 })),
|
|
2189
|
+
_.div({ class: "jss-date-header-center" }, monthSelect, yearSelect),
|
|
2190
|
+
_.button({
|
|
2191
|
+
type: "button",
|
|
2192
|
+
class: "jss-date-nav",
|
|
2193
|
+
onClick: () => {
|
|
2194
|
+
viewMonth = shiftMonths(viewMonth, 1);
|
|
2195
|
+
props.onNavigate?.({ month: viewMonth.getMonth() + 1, year: viewMonth.getFullYear() });
|
|
2196
|
+
renderPanel();
|
|
2197
|
+
}
|
|
2198
|
+
}, UI.Icon({ name: "chevron_right", size: 16 }))
|
|
2199
|
+
);
|
|
2200
|
+
|
|
2201
|
+
const shortcuts = _.div({ class: "jss-date-shortcuts" });
|
|
2202
|
+
const shortcutList = uiUnwrap(props.shortcuts ?? props.presets) || [];
|
|
2203
|
+
shortcutList.forEach((item, index) => {
|
|
2204
|
+
if (!item) return;
|
|
2205
|
+
const rawValue = typeof item.value === "function" ? item.value({ today: todayIso() }) : item.value;
|
|
2206
|
+
const label = item.label ?? item.text ?? `Preset ${index + 1}`;
|
|
2207
|
+
shortcuts.appendChild(_.button({
|
|
2208
|
+
type: "button",
|
|
2209
|
+
class: "jss-date-shortcut",
|
|
2210
|
+
onClick: () => setWorkingOrCommit(rawValue, null, { preserveTime: isTimeEnabled(), timeValue: getCurrentTimeValue() })
|
|
2211
|
+
}, label));
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
const months = _.div({ class: "jss-date-months" });
|
|
2215
|
+
for (let index = 0; index < getMonthsToShow(); index += 1) {
|
|
2216
|
+
months.appendChild(renderMonth(shiftMonths(viewMonth, index), index));
|
|
2217
|
+
}
|
|
2218
|
+
const footerDisplayValue = formatDisplayValue(pickerValue, getCurrentTimeValue());
|
|
2219
|
+
const footerValue = _.div({ class: "jss-date-value" },
|
|
2220
|
+
...renderSlotToArray(
|
|
2221
|
+
slots,
|
|
2222
|
+
"value",
|
|
2223
|
+
{ value: pickerValue, displayValue: footerDisplayValue, mode, timeValue: getCurrentTimeValue() },
|
|
2224
|
+
footerDisplayValue || renderSlotToArray(slots, "default", {}, children)
|
|
2225
|
+
)
|
|
2226
|
+
);
|
|
2227
|
+
const headerTime = _.div({ class: "jss-time-header" });
|
|
2228
|
+
const columnsTime = _.div({ class: "jss-time-columns-2" },
|
|
2229
|
+
_.div({ class: "jss-time-column-title" }, uiUnwrap(props.hoursLabel) || "Ore"),
|
|
2230
|
+
_.div({ class: "jss-time-column-title" }, uiUnwrap(props.minutesLabel) || "Min")
|
|
2231
|
+
);
|
|
2232
|
+
headerTime.appendChild(columnsTime);
|
|
2233
|
+
const bodyTime = isTimeEnabled()
|
|
2234
|
+
? uiCreateTimePickerSection({
|
|
2235
|
+
props: {
|
|
2236
|
+
min: uiUnwrap(props.timeMin ?? props.minTime),
|
|
2237
|
+
max: uiUnwrap(props.timeMax ?? props.maxTime),
|
|
2238
|
+
use12h: uiUnwrap(props.time12h ?? props.use12h ?? props.ampm),
|
|
2239
|
+
hoursLabel: uiUnwrap(props.timeHoursLabel) || "Ore",
|
|
2240
|
+
minutesLabel: uiUnwrap(props.timeMinutesLabel) || "Min",
|
|
2241
|
+
secondsLabel: uiUnwrap(props.timeSecondsLabel) || "Sec"
|
|
2242
|
+
},
|
|
2243
|
+
slots,
|
|
2244
|
+
slotPrefix: "time",
|
|
2245
|
+
value: getCurrentTimeValue(),
|
|
2246
|
+
withSeconds: false,
|
|
2247
|
+
minuteStep: 1,
|
|
2248
|
+
secondStep: getTimeSecondStep(),
|
|
2249
|
+
disabled: !!uiUnwrap(props.disabled),
|
|
2250
|
+
readonly: !!uiUnwrap(props.readonly),
|
|
2251
|
+
embedded: true,
|
|
2252
|
+
pointIcon: uiUnwrap(props.timePointIcon),
|
|
2253
|
+
shortcuts: uiUnwrap(props.timeShortcuts ?? props.timePresets),
|
|
2254
|
+
onSelect: (nextTime) => {
|
|
2255
|
+
const scrollState = uiCaptureTimeColumnScrollState(panelRoot);
|
|
2256
|
+
setTimeValue(nextTime, null);
|
|
2257
|
+
scrollTimeColumnsToSelection({ scrollState });
|
|
2258
|
+
}
|
|
2259
|
+
})
|
|
2260
|
+
: null;
|
|
2261
|
+
const footerInfo = _.div({ class: "jss-date-footer-info" },
|
|
2262
|
+
footerValue
|
|
2263
|
+
);
|
|
2264
|
+
|
|
2265
|
+
const footer = _.div({ class: uiClass(["jss-date-footer", uiWhen(isTimeEnabled(), "has-time")]) },
|
|
2266
|
+
footerInfo,
|
|
2267
|
+
_.div({ class: "jss-date-actions" },
|
|
2268
|
+
_.button({ type: "button", class: "jss-date-action", onClick: jumpToToday }, uiUnwrap(props.todayLabel) || "Oggi"),
|
|
2269
|
+
uiUnwrap(props.clearable) !== false
|
|
2270
|
+
? _.button({ type: "button", class: "jss-date-action", onClick: clearValue }, uiUnwrap(props.clearLabel) || "Reset")
|
|
2271
|
+
: null,
|
|
2272
|
+
uiUnwrap(props.confirm)
|
|
2273
|
+
? _.button({ type: "button", class: "jss-date-action is-primary", onClick: applyWorkingValue }, uiUnwrap(props.applyLabel) || "Applica")
|
|
2274
|
+
: null
|
|
2275
|
+
)
|
|
2276
|
+
);
|
|
2277
|
+
|
|
2278
|
+
const footerTimer = _.div(_.button({ type: "button", class: "jss-date-action jss-w-lg", onClick: jumpToNow }, uiUnwrap(props.nowLabel) || "Ora"));
|
|
2279
|
+
|
|
2280
|
+
const contentTimer = isTimeEnabled() ? _.div({ class: "jss-date-time" }, headerTime, bodyTime, footerTimer) : null;
|
|
2281
|
+
|
|
2282
|
+
panelRoot.replaceChildren(
|
|
2283
|
+
_.div({
|
|
2284
|
+
class: "jss-date-panel-root",
|
|
2285
|
+
onKeydown: (event) => {
|
|
2286
|
+
if (event.key === "Escape") {
|
|
2287
|
+
event.preventDefault();
|
|
2288
|
+
if (isCalendar) props.onEscape?.(event);
|
|
2289
|
+
else closePanel();
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
},
|
|
2293
|
+
header,
|
|
2294
|
+
shortcutList.length ? shortcuts : null,
|
|
2295
|
+
_.div({ class: 'jss-data-time' }, months, contentTimer),
|
|
2296
|
+
footer
|
|
2297
|
+
)
|
|
2298
|
+
);
|
|
2299
|
+
};
|
|
2300
|
+
|
|
2301
|
+
function openPanel() {
|
|
2302
|
+
if (entry || uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return entry;
|
|
2303
|
+
workingValue = cloneValue(localValue);
|
|
2304
|
+
workingTimeValue = uiCloneTimeParts(localTimeValue);
|
|
2305
|
+
hoverDate = null;
|
|
2306
|
+
mouseSelectedDate = null;
|
|
2307
|
+
syncViewMonth(workingValue);
|
|
2308
|
+
entry = JSswift.overlay.open(({ close }) => {
|
|
2309
|
+
const fallback = (mode === "range" || mode === "range-multiple") ? 2 : 1;
|
|
2310
|
+
const raw = Number(uiUnwrap(props.monthsToShow) ?? fallback);
|
|
2311
|
+
panelRoot = _.div({ class: uiClassStatic(["jss-date-panel", uiUnwrap(sizeValue), uiWhen(raw > 1, "multi-month"), props.panelClass]) });
|
|
2312
|
+
renderPanel();
|
|
2313
|
+
return panelRoot;
|
|
2314
|
+
}, {
|
|
2315
|
+
type: "date",
|
|
2316
|
+
anchorEl: field._control || displayInput,
|
|
2317
|
+
placement: props.placement || "bottom-start",
|
|
2318
|
+
offsetX: props.offsetX ?? 0,
|
|
2319
|
+
offsetY: props.offsetY ?? props.offset ?? 8,
|
|
2320
|
+
backdrop: false,
|
|
2321
|
+
lockScroll: false,
|
|
2322
|
+
trapFocus: false,
|
|
2323
|
+
closeOnOutside: props.closeOnOutside !== false,
|
|
2324
|
+
closeOnBackdrop: false,
|
|
2325
|
+
closeOnEsc: true,
|
|
2326
|
+
autoFocus: false,
|
|
2327
|
+
className: uiClassStatic(["jss-date-overlay", props.panelClass, uiWhen(isTimeEnabled(), "has-time")]),
|
|
2328
|
+
onClose: () => {
|
|
2329
|
+
entry = null;
|
|
2330
|
+
panelRoot = null;
|
|
2331
|
+
hoverDate = null;
|
|
2332
|
+
syncDisplay();
|
|
2333
|
+
props.onClose?.();
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
if (props.panelStyle) Object.assign(entry.panel.style, props.panelStyle);
|
|
2337
|
+
overlayEnter(entry);
|
|
2338
|
+
syncDisplay();
|
|
2339
|
+
props.onOpen?.();
|
|
2340
|
+
return entry;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
function closePanel() {
|
|
2344
|
+
if (!entry) return;
|
|
2345
|
+
mouseSelectedDate = null;
|
|
2346
|
+
const toClose = entry;
|
|
2347
|
+
overlayLeave(toClose, () => JSswift.overlay.close(toClose.id));
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
if (isCalendar) {
|
|
2351
|
+
const fallback = (mode === "range" || mode === "range-multiple") ? 2 : 1;
|
|
2352
|
+
const raw = Number(uiUnwrap(props.monthsToShow) ?? fallback);
|
|
2353
|
+
panelRoot = _.div({
|
|
2354
|
+
class: uiClassStatic([
|
|
2355
|
+
"jss-date-panel",
|
|
2356
|
+
"jss-date-calendar",
|
|
2357
|
+
uiUnwrap(sizeValue),
|
|
2358
|
+
uiWhen(raw > 1, "multi-month"),
|
|
2359
|
+
props.panelClass,
|
|
2360
|
+
props.class
|
|
2361
|
+
]),
|
|
2362
|
+
style: props.style
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
if (!isCalendar) displayInput.addEventListener("focus", (event) => {
|
|
2367
|
+
props.onFocus?.(event);
|
|
2368
|
+
if (props.openOnFocus !== false) openPanel();
|
|
2369
|
+
});
|
|
2370
|
+
if (!isCalendar) displayInput.addEventListener("click", (event) => {
|
|
2371
|
+
props.onClick?.(event);
|
|
2372
|
+
openPanel();
|
|
2373
|
+
});
|
|
2374
|
+
if (!isCalendar) displayInput.addEventListener("keydown", (event) => {
|
|
2375
|
+
if (event.key === "Escape") {
|
|
2376
|
+
if (entry) {
|
|
2377
|
+
event.preventDefault();
|
|
2378
|
+
closePanel();
|
|
2379
|
+
}
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
|
2383
|
+
event.preventDefault();
|
|
2384
|
+
openPanel();
|
|
2385
|
+
}
|
|
2386
|
+
if ((event.key === "Backspace" || event.key === "Delete") && !uiUnwrap(props.manualInput) && uiUnwrap(props.clearable) !== false) {
|
|
2387
|
+
event.preventDefault();
|
|
2388
|
+
clearValue();
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
if (!isCalendar && uiUnwrap(props.manualInput)) {
|
|
2392
|
+
displayInput.addEventListener("input", (event) => {
|
|
2393
|
+
props.onTyping?.(displayInput.value, event);
|
|
2394
|
+
});
|
|
2395
|
+
displayInput.addEventListener("change", (event) => {
|
|
2396
|
+
const parsed = parseTypedValue(displayInput.value);
|
|
2397
|
+
setDateValue(parsed, event, { timeValue: uiExtractTimeFromValue(displayInput.value, getTimeOptions()), preserveTime: !uiExtractTimeFromValue(displayInput.value, getTimeOptions()) });
|
|
2398
|
+
});
|
|
2399
|
+
displayInput.addEventListener("blur", (event) => {
|
|
2400
|
+
props.onBlur?.(event);
|
|
2401
|
+
const parsed = parseTypedValue(displayInput.value);
|
|
2402
|
+
if (displayInput.value === "") clearValue();
|
|
2403
|
+
else setDateValue(parsed, event, { timeValue: uiExtractTimeFromValue(displayInput.value, getTimeOptions()), preserveTime: !uiExtractTimeFromValue(displayInput.value, getTimeOptions()) });
|
|
2404
|
+
});
|
|
2405
|
+
} else if (!isCalendar) {
|
|
2406
|
+
displayInput.addEventListener("blur", (event) => props.onBlur?.(event));
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
if (model) {
|
|
2410
|
+
model.watch((next) => {
|
|
2411
|
+
const normalized = normalizeValue(next);
|
|
2412
|
+
const nextTime = resolveDateTimeTimeValue(next);
|
|
2413
|
+
if (serializeCurrentValue(normalized, nextTime) === serializeCurrentValue(localValue, localTimeValue)) return;
|
|
2414
|
+
localValue = normalized;
|
|
2415
|
+
workingValue = cloneValue(localValue);
|
|
2416
|
+
localTimeValue = uiCloneTimeParts(nextTime);
|
|
2417
|
+
workingTimeValue = uiCloneTimeParts(nextTime);
|
|
2418
|
+
hoverDate = null;
|
|
2419
|
+
syncViewMonth(localValue);
|
|
2420
|
+
syncDisplay();
|
|
2421
|
+
renderPanel();
|
|
2422
|
+
}, isCalendar ? "UI.Calendar:watch" : "UI.Datepicker:watch");
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
JSswift.reactive.effect(() => {
|
|
2426
|
+
if (!model && props.value != null) {
|
|
2427
|
+
localValue = normalizeValue(uiUnwrap(props.value));
|
|
2428
|
+
workingValue = cloneValue(localValue);
|
|
2429
|
+
localTimeValue = resolveDateTimeTimeValue(uiUnwrap(props.value));
|
|
2430
|
+
workingTimeValue = uiCloneTimeParts(localTimeValue);
|
|
2431
|
+
} else if (isTimeEnabled() && props.timeValue != null) {
|
|
2432
|
+
localTimeValue = resolveDateTimeTimeValue(uiUnwrap(props.timeValue));
|
|
2433
|
+
workingTimeValue = uiCloneTimeParts(localTimeValue);
|
|
2434
|
+
}
|
|
2435
|
+
syncDisplay();
|
|
2436
|
+
renderPanel();
|
|
2437
|
+
if (entry && uiUnwrap(props.disabled)) closePanel();
|
|
2438
|
+
}, isCalendar ? "UI.Calendar:render" : "UI.Datepicker:render");
|
|
2439
|
+
|
|
2440
|
+
if (isCalendar) {
|
|
2441
|
+
panelRoot._calendar = panelRoot;
|
|
2442
|
+
panelRoot._date = panelRoot;
|
|
2443
|
+
panelRoot._getValue = () => exportCurrentValue(localValue, localTimeValue);
|
|
2444
|
+
panelRoot._setValue = (value) => setDateValue(value, null, { emit: false, timeValue: resolveDateTimeTimeValue(value) });
|
|
2445
|
+
panelRoot._clear = clearValue;
|
|
2446
|
+
panelRoot._select = (value) => {
|
|
2447
|
+
const iso = normalizeDateOnly(value);
|
|
2448
|
+
if (iso) selectDate(iso, null);
|
|
2449
|
+
};
|
|
2450
|
+
panelRoot._time = () => uiCloneTimeParts(localTimeValue);
|
|
2451
|
+
return panelRoot;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
field._input = displayInput;
|
|
2455
|
+
field._date = displayInput;
|
|
2456
|
+
field._open = openPanel;
|
|
2457
|
+
field._close = closePanel;
|
|
2458
|
+
field._getValue = () => exportCurrentValue(localValue, localTimeValue);
|
|
2459
|
+
field._setValue = (value) => setDateValue(value, null, { emit: false, timeValue: resolveDateTimeTimeValue(value) });
|
|
2460
|
+
field._time = () => uiCloneTimeParts(localTimeValue);
|
|
2461
|
+
field._panel = () => entry?.panel || null;
|
|
2462
|
+
|
|
2463
|
+
return field;
|
|
2464
|
+
};
|
|
2465
|
+
UI.Datepicker = (...args) => buildDateControl(args);
|
|
2466
|
+
UI.Calendar = (...args) => buildDateControl(args, { calendar: true });
|
|
2467
|
+
UI.Date = UI.Datepicker;
|
|
2468
|
+
JSswift.ui.Datepicker = UI.Datepicker;
|
|
2469
|
+
JSswift.ui.Calendar = UI.Calendar;
|
|
2470
|
+
JSswift.ui.Date = UI.Date;
|
|
2471
|
+
|
|
2472
|
+
// Esempio: JSswift.ui.Datepicker({ value: "2024-01-01" })
|
|
2473
|
+
|
|
2474
|
+
UI.Time = (...args) => {
|
|
2475
|
+
const { props, children } = JSswift.uiNormalizeArgs(args);
|
|
2476
|
+
const slots = props.slots || {};
|
|
2477
|
+
const sizeValue = uiComputed(props.size, () => {
|
|
2478
|
+
const value = String(uiUnwrap(props.size) || "").toLowerCase();
|
|
2479
|
+
return ["xs", "sm", "md", "lg", "xl"].includes(value) ? `jss-size-${value}` : "";
|
|
2480
|
+
});
|
|
2481
|
+
const getTimeOptions = () => {
|
|
2482
|
+
const detectedTime = uiExportTimeValue(
|
|
2483
|
+
uiExtractTimeFromValue(model ? model.get() : uiUnwrap(props.value) ?? initialRawValue, { withSeconds: true }),
|
|
2484
|
+
{ withSeconds: true }
|
|
2485
|
+
);
|
|
2486
|
+
return {
|
|
2487
|
+
withSeconds: !!uiUnwrap(props.withSeconds ?? props.showSeconds) || /:\d{2}:\d{2}$/.test(detectedTime),
|
|
2488
|
+
min: uiUnwrap(props.min ?? props.minTime),
|
|
2489
|
+
max: uiUnwrap(props.max ?? props.maxTime),
|
|
2490
|
+
use12h: uiUnwrap(props.use12h ?? props.ampm)
|
|
2491
|
+
};
|
|
2492
|
+
};
|
|
2493
|
+
const getMinuteStep = () => {
|
|
2494
|
+
const raw = Number(uiUnwrap(props.minuteStep ?? 1));
|
|
2495
|
+
return Math.max(1, Math.min(30, Number.isFinite(raw) ? raw : 5));
|
|
2496
|
+
};
|
|
2497
|
+
const getSecondStep = () => {
|
|
2498
|
+
const raw = Number(uiUnwrap(props.secondStep ?? 1));
|
|
2499
|
+
return Math.max(1, Math.min(30, Number.isFinite(raw) ? raw : 5));
|
|
2500
|
+
};
|
|
2501
|
+
const valueBinding = props.model || ((uiIsSignal(props.value) || uiIsRod(props.value)) ? props.value : null);
|
|
2502
|
+
const model = resolveModel(valueBinding, "UI.Time:model");
|
|
2503
|
+
const initialRawValue = model ? model.get() : uiUnwrap(props.value);
|
|
2504
|
+
const parseTypedValue = (raw) => uiExtractTimeFromValue(raw, getTimeOptions());
|
|
2505
|
+
const formatDisplayValue = (value) => uiFormatTimeDisplay(value, getTimeOptions());
|
|
2506
|
+
const exportValue = (value) => uiExportTimeValue(value, getTimeOptions());
|
|
2507
|
+
const serializeValue = (value) => JSON.stringify(exportValue(value));
|
|
2508
|
+
|
|
2509
|
+
let localValue = uiCloneTimeParts(parseTypedValue(initialRawValue));
|
|
2510
|
+
let workingValue = uiCloneTimeParts(localValue);
|
|
2511
|
+
let entry = null;
|
|
2512
|
+
let panelRoot = null;
|
|
2513
|
+
|
|
2514
|
+
const fieldProps = {
|
|
2515
|
+
...props,
|
|
2516
|
+
label: props.label ?? props.placeholder
|
|
2517
|
+
};
|
|
2518
|
+
const hasFloatingLabel = fieldProps.label != null || JSswift.ui.getSlot(slots, "label") != null;
|
|
2519
|
+
|
|
2520
|
+
const displayInput = _.input({
|
|
2521
|
+
class: uiClass(["jss-input", "jss-time-display", sizeValue, uiWhen(props.manualInput, "is-manual"), props.inputClass]),
|
|
2522
|
+
type: "text",
|
|
2523
|
+
autocomplete: "off",
|
|
2524
|
+
placeholder: hasFloatingLabel ? "" : (props.placeholder ?? "Seleziona orario"),
|
|
2525
|
+
readOnly: !uiUnwrap(props.manualInput),
|
|
2526
|
+
disabled: !!uiUnwrap(props.disabled),
|
|
2527
|
+
value: formatDisplayValue(localValue)
|
|
2528
|
+
});
|
|
2529
|
+
displayInput.setAttribute("aria-haspopup", "dialog");
|
|
2530
|
+
const hiddenHost = _.div({ style: { display: "contents" } });
|
|
2531
|
+
const controlNode = _.div({ class: "jss-time-control", style: { display: "contents" } }, displayInput, hiddenHost);
|
|
2532
|
+
|
|
2533
|
+
const field = UI.FormField({
|
|
2534
|
+
...fieldProps,
|
|
2535
|
+
iconRight: props.iconRight ?? "schedule",
|
|
2536
|
+
control: controlNode,
|
|
2537
|
+
getValue: () => displayInput.value,
|
|
2538
|
+
onClear: () => {
|
|
2539
|
+
if (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return;
|
|
2540
|
+
setTimeState(null, null, { commit: true });
|
|
2541
|
+
if (entry) closePanel();
|
|
2542
|
+
},
|
|
2543
|
+
onFocus: () => displayInput.focus()
|
|
2544
|
+
});
|
|
2545
|
+
|
|
2546
|
+
const getCurrentValue = () => uiUnwrap(props.confirm) ? workingValue : localValue;
|
|
2547
|
+
const syncHiddenInputs = () => {
|
|
2548
|
+
hiddenHost.replaceChildren();
|
|
2549
|
+
const baseName = uiUnwrap(props.name);
|
|
2550
|
+
if (!baseName) return;
|
|
2551
|
+
hiddenHost.appendChild(_.input({ type: "hidden", name: baseName, value: exportValue(localValue) }));
|
|
2552
|
+
};
|
|
2553
|
+
const syncDisplay = () => {
|
|
2554
|
+
displayInput.readOnly = !uiUnwrap(props.manualInput);
|
|
2555
|
+
displayInput.disabled = !!uiUnwrap(props.disabled);
|
|
2556
|
+
displayInput.setAttribute("aria-expanded", entry ? "true" : "false");
|
|
2557
|
+
displayInput.value = formatDisplayValue(localValue);
|
|
2558
|
+
syncHiddenInputs();
|
|
2559
|
+
field._refresh?.();
|
|
2560
|
+
};
|
|
2561
|
+
const setTimeState = (nextValue, event, options = {}) => {
|
|
2562
|
+
const normalized = uiCloneTimeParts(parseTypedValue(nextValue));
|
|
2563
|
+
if (uiUnwrap(props.confirm) && options.commit !== true) {
|
|
2564
|
+
workingValue = uiCloneTimeParts(normalized);
|
|
2565
|
+
renderPanel();
|
|
2566
|
+
return normalized;
|
|
2567
|
+
}
|
|
2568
|
+
const prev = serializeValue(localValue);
|
|
2569
|
+
localValue = uiCloneTimeParts(normalized);
|
|
2570
|
+
workingValue = uiCloneTimeParts(normalized);
|
|
2571
|
+
syncDisplay();
|
|
2572
|
+
renderPanel();
|
|
2573
|
+
if (model && options.fromModel !== true) model.set(exportValue(normalized));
|
|
2574
|
+
const nextSerialized = serializeValue(normalized);
|
|
2575
|
+
if (options.emit !== false && nextSerialized !== prev) {
|
|
2576
|
+
const emitted = exportValue(normalized);
|
|
2577
|
+
props.onInput?.(emitted, event);
|
|
2578
|
+
props.onChange?.(emitted, event);
|
|
2579
|
+
}
|
|
2580
|
+
return normalized;
|
|
2581
|
+
};
|
|
2582
|
+
const shouldCloseOnSelect = (meta) => {
|
|
2583
|
+
if (uiUnwrap(props.confirm)) return false;
|
|
2584
|
+
if (props.closeOnSelect !== true) return false;
|
|
2585
|
+
if (getTimeOptions().withSeconds) return meta?.part === "second";
|
|
2586
|
+
return meta?.part === "minute";
|
|
2587
|
+
};
|
|
2588
|
+
const scrollTimeColumnsToSelection = (options = {}) => {
|
|
2589
|
+
uiCenterTimeColumnsToSelection(panelRoot, options);
|
|
2590
|
+
};
|
|
2591
|
+
const jumpToNow = () => {
|
|
2592
|
+
const scrollState = uiCaptureTimeColumnScrollState(panelRoot);
|
|
2593
|
+
setTimeState(new Date(), null, { commit: !uiUnwrap(props.confirm) });
|
|
2594
|
+
scrollTimeColumnsToSelection({ scrollState });
|
|
2595
|
+
if (!uiUnwrap(props.confirm) && props.closeOnSelect === true) closePanel();
|
|
2596
|
+
};
|
|
2597
|
+
const applyWorkingValue = () => {
|
|
2598
|
+
if (!uiUnwrap(props.confirm)) return;
|
|
2599
|
+
setTimeState(workingValue, null, { commit: true });
|
|
2600
|
+
if (props.closeOnSelect !== false) closePanel();
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
const renderPanel = () => {
|
|
2604
|
+
if (!panelRoot) return;
|
|
2605
|
+
const currentValue = getCurrentValue();
|
|
2606
|
+
const footerDisplayValue = formatDisplayValue(currentValue);
|
|
2607
|
+
const header = _.div({ class: "jss-time-header" });
|
|
2608
|
+
const columns = _.div({ class: "jss-time-columns" },
|
|
2609
|
+
_.div({ class: "jss-time-column-title" }, uiUnwrap(props.hoursLabel) || "Ore"),
|
|
2610
|
+
_.div({ class: "jss-time-column-title" }, uiUnwrap(props.minutesLabel) || "Min")
|
|
2611
|
+
);
|
|
2612
|
+
const withSeconds = getTimeOptions().withSeconds;
|
|
2613
|
+
if (withSeconds) {
|
|
2614
|
+
columns.appendChild(_.div({ class: "jss-time-column-title" }, uiUnwrap(props.secondsLabel) || "Sec"));
|
|
2615
|
+
}
|
|
2616
|
+
header.appendChild(columns);
|
|
2617
|
+
const body = uiCreateTimePickerSection({
|
|
2618
|
+
props: {
|
|
2619
|
+
min: uiUnwrap(props.min ?? props.minTime),
|
|
2620
|
+
max: uiUnwrap(props.max ?? props.maxTime),
|
|
2621
|
+
use12h: uiUnwrap(props.use12h ?? props.ampm),
|
|
2622
|
+
hoursLabel: uiUnwrap(props.hoursLabel) || "Ore",
|
|
2623
|
+
minutesLabel: uiUnwrap(props.minutesLabel) || "Min",
|
|
2624
|
+
secondsLabel: uiUnwrap(props.secondsLabel) || "Sec"
|
|
2625
|
+
},
|
|
2626
|
+
slots,
|
|
2627
|
+
value: currentValue,
|
|
2628
|
+
withSeconds: withSeconds,
|
|
2629
|
+
minuteStep: getMinuteStep(),
|
|
2630
|
+
secondStep: getSecondStep(),
|
|
2631
|
+
disabled: !!uiUnwrap(props.disabled),
|
|
2632
|
+
readonly: !!uiUnwrap(props.readonly),
|
|
2633
|
+
pointIcon: uiUnwrap(props.pointIcon),
|
|
2634
|
+
shortcuts: uiUnwrap(props.shortcuts ?? props.presets),
|
|
2635
|
+
onSelect: (nextTime, meta) => {
|
|
2636
|
+
const scrollState = uiCaptureTimeColumnScrollState(panelRoot);
|
|
2637
|
+
setTimeState(nextTime, null);
|
|
2638
|
+
scrollTimeColumnsToSelection({ scrollState });
|
|
2639
|
+
if (shouldCloseOnSelect(meta)) closePanel();
|
|
2640
|
+
}
|
|
2641
|
+
});
|
|
2642
|
+
const footer = _.div({ class: "jss-time-footer" },
|
|
2643
|
+
_.div({ class: "jss-time-value" },
|
|
2644
|
+
...renderSlotToArray(
|
|
2645
|
+
slots,
|
|
2646
|
+
"value",
|
|
2647
|
+
{ value: currentValue, displayValue: footerDisplayValue },
|
|
2648
|
+
footerDisplayValue || renderSlotToArray(slots, "default", {}, children)
|
|
2649
|
+
)
|
|
2650
|
+
),
|
|
2651
|
+
_.div({ class: "jss-time-actions" },
|
|
2652
|
+
_.button({ type: "button", class: "jss-time-action", onClick: jumpToNow }, uiUnwrap(props.nowLabel) || "Ora"),
|
|
2653
|
+
uiUnwrap(props.clearable) !== false
|
|
2654
|
+
? _.button({ type: "button", class: "jss-time-action", onClick: () => setTimeState(null, null, { commit: !uiUnwrap(props.confirm) }) }, uiUnwrap(props.clearLabel) || "Reset")
|
|
2655
|
+
: null,
|
|
2656
|
+
uiUnwrap(props.confirm)
|
|
2657
|
+
? _.button({ type: "button", class: "jss-time-action is-primary", onClick: applyWorkingValue }, uiUnwrap(props.applyLabel) || "Applica")
|
|
2658
|
+
: null
|
|
2659
|
+
)
|
|
2660
|
+
);
|
|
2661
|
+
panelRoot.replaceChildren(_.div({
|
|
2662
|
+
class: "jss-time-panel-root",
|
|
2663
|
+
onKeydown: (event) => {
|
|
2664
|
+
if (event.key === "Escape") {
|
|
2665
|
+
event.preventDefault();
|
|
2666
|
+
closePanel();
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
}, header, body, footer));
|
|
2670
|
+
};
|
|
2671
|
+
|
|
2672
|
+
function openPanel() {
|
|
2673
|
+
if (entry || uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return entry;
|
|
2674
|
+
workingValue = uiCloneTimeParts(localValue);
|
|
2675
|
+
entry = JSswift.overlay.open(() => {
|
|
2676
|
+
panelRoot = _.div({ class: uiClassStatic(["jss-time-panel", uiUnwrap(sizeValue), props.panelClass]) });
|
|
2677
|
+
renderPanel();
|
|
2678
|
+
return panelRoot;
|
|
2679
|
+
}, {
|
|
2680
|
+
type: "time",
|
|
2681
|
+
anchorEl: field._control || displayInput,
|
|
2682
|
+
placement: props.placement || "bottom-start",
|
|
2683
|
+
offsetX: props.offsetX ?? 0,
|
|
2684
|
+
offsetY: props.offsetY ?? props.offset ?? 8,
|
|
2685
|
+
backdrop: false,
|
|
2686
|
+
lockScroll: false,
|
|
2687
|
+
trapFocus: false,
|
|
2688
|
+
closeOnOutside: props.closeOnOutside !== false,
|
|
2689
|
+
closeOnBackdrop: false,
|
|
2690
|
+
closeOnEsc: true,
|
|
2691
|
+
autoFocus: false,
|
|
2692
|
+
className: uiClassStatic(["jss-time-overlay", props.panelClass]),
|
|
2693
|
+
onClose: () => {
|
|
2694
|
+
entry = null;
|
|
2695
|
+
panelRoot = null;
|
|
2696
|
+
syncDisplay();
|
|
2697
|
+
props.onClose?.();
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
if (props.panelStyle) Object.assign(entry.panel.style, props.panelStyle);
|
|
2701
|
+
overlayEnter(entry);
|
|
2702
|
+
scrollTimeColumnsToSelection();
|
|
2703
|
+
syncDisplay();
|
|
2704
|
+
props.onOpen?.();
|
|
2705
|
+
return entry;
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
function closePanel() {
|
|
2709
|
+
if (!entry) return;
|
|
2710
|
+
const toClose = entry;
|
|
2711
|
+
overlayLeave(toClose, () => JSswift.overlay.close(toClose.id));
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
displayInput.addEventListener("focus", (event) => {
|
|
2715
|
+
props.onFocus?.(event);
|
|
2716
|
+
if (props.openOnFocus !== false) openPanel();
|
|
2717
|
+
});
|
|
2718
|
+
displayInput.addEventListener("click", (event) => {
|
|
2719
|
+
props.onClick?.(event);
|
|
2720
|
+
openPanel();
|
|
2721
|
+
});
|
|
2722
|
+
displayInput.addEventListener("keydown", (event) => {
|
|
2723
|
+
if (event.key === "Escape") {
|
|
2724
|
+
if (entry) {
|
|
2725
|
+
event.preventDefault();
|
|
2726
|
+
closePanel();
|
|
2727
|
+
}
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
|
2731
|
+
event.preventDefault();
|
|
2732
|
+
openPanel();
|
|
2733
|
+
}
|
|
2734
|
+
if ((event.key === "Backspace" || event.key === "Delete") && !uiUnwrap(props.manualInput) && uiUnwrap(props.clearable) !== false) {
|
|
2735
|
+
event.preventDefault();
|
|
2736
|
+
setTimeState(null, event, { commit: true });
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
if (uiUnwrap(props.manualInput)) {
|
|
2740
|
+
displayInput.addEventListener("input", (event) => props.onTyping?.(displayInput.value, event));
|
|
2741
|
+
displayInput.addEventListener("change", (event) => setTimeState(displayInput.value, event));
|
|
2742
|
+
displayInput.addEventListener("blur", (event) => {
|
|
2743
|
+
props.onBlur?.(event);
|
|
2744
|
+
if (displayInput.value === "") setTimeState(null, event);
|
|
2745
|
+
else setTimeState(displayInput.value, event);
|
|
2746
|
+
});
|
|
2747
|
+
} else {
|
|
2748
|
+
displayInput.addEventListener("blur", (event) => props.onBlur?.(event));
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
if (model) {
|
|
2752
|
+
model.watch((next) => {
|
|
2753
|
+
const normalized = uiCloneTimeParts(parseTypedValue(next));
|
|
2754
|
+
if (serializeValue(normalized) === serializeValue(localValue)) return;
|
|
2755
|
+
localValue = uiCloneTimeParts(normalized);
|
|
2756
|
+
workingValue = uiCloneTimeParts(normalized);
|
|
2757
|
+
syncDisplay();
|
|
2758
|
+
renderPanel();
|
|
2759
|
+
}, "UI.Time:watch");
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
JSswift.reactive.effect(() => {
|
|
2763
|
+
if (!model && props.value != null) {
|
|
2764
|
+
localValue = uiCloneTimeParts(parseTypedValue(uiUnwrap(props.value)));
|
|
2765
|
+
workingValue = uiCloneTimeParts(localValue);
|
|
2766
|
+
}
|
|
2767
|
+
syncDisplay();
|
|
2768
|
+
renderPanel();
|
|
2769
|
+
if (entry && uiUnwrap(props.disabled)) closePanel();
|
|
2770
|
+
}, "UI.Time:render");
|
|
2771
|
+
|
|
2772
|
+
field._input = displayInput;
|
|
2773
|
+
field._time = displayInput;
|
|
2774
|
+
field._open = openPanel;
|
|
2775
|
+
field._close = closePanel;
|
|
2776
|
+
field._getValue = () => exportValue(localValue);
|
|
2777
|
+
field._setValue = (value) => setTimeState(value, null, { emit: false, commit: true });
|
|
2778
|
+
field._panel = () => entry?.panel || null;
|
|
2779
|
+
|
|
2780
|
+
return field;
|
|
2781
|
+
};
|
|
2782
|
+
|
|
2783
|
+
// Esempio: JSswift.ui.Time({ value: "09:30" })
|
|
2784
|
+
|