nexus-shared 1.1.21 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +262 -0
- package/LICENSE +21 -0
- package/README.md +1000 -0
- package/dist/Client.Index.d.ts +55 -0
- package/dist/Client.Index.js +60 -0
- package/dist/Components/Chats/Chat.d.ts +28 -0
- package/dist/Components/Chats/Chat.js +19 -0
- package/dist/Components/Chats/ChatButton.d.ts +26 -0
- package/dist/Components/Chats/ChatButton.js +41 -0
- package/dist/Components/Chats/ChatComposer.d.ts +41 -0
- package/dist/Components/Chats/ChatComposer.js +177 -0
- package/dist/Components/Chats/ChatConversations.d.ts +35 -0
- package/dist/Components/Chats/ChatConversations.js +38 -0
- package/dist/Components/Chats/ChatPanel.d.ts +66 -0
- package/dist/Components/Chats/ChatPanel.js +94 -0
- package/dist/Components/Chats/ChatParts.d.ts +87 -0
- package/dist/Components/Chats/ChatParts.js +100 -0
- package/dist/Components/Chats/ChatThread.d.ts +22 -0
- package/dist/Components/Chats/ChatThread.js +96 -0
- package/dist/Components/Documents/Button.d.ts +29 -0
- package/dist/Components/Documents/Button.js +11 -0
- package/dist/Components/Documents/Menu.d.ts +102 -0
- package/dist/Components/Documents/Menu.js +242 -0
- package/dist/Components/Documents/SplitButton.d.ts +64 -0
- package/dist/Components/Documents/SplitButton.js +129 -0
- package/dist/Components/Documents/TabButtons.d.ts +80 -0
- package/dist/Components/Documents/TabButtons.js +38 -0
- package/dist/Components/Forms/ApiForm.d.ts +23 -0
- package/dist/Components/Forms/ApiForm.js +100 -0
- package/dist/Components/Forms/Crud.d.ts +17 -0
- package/dist/Components/Forms/Crud.js +708 -0
- package/dist/Components/Forms/CrudParts.d.ts +15 -0
- package/dist/Components/Forms/CrudParts.js +36 -0
- package/dist/Components/Forms/ExcelImport.d.ts +42 -0
- package/dist/Components/Forms/ExcelImport.js +190 -0
- package/dist/Components/Forms/Form.d.ts +52 -0
- package/dist/Components/Forms/Form.js +323 -0
- package/dist/Components/Forms/SubmitForm.d.ts +65 -0
- package/dist/Components/Forms/SubmitForm.js +198 -0
- package/dist/Components/Inputs/Calendar.d.ts +69 -0
- package/dist/Components/Inputs/Calendar.js +258 -0
- package/dist/Components/Inputs/CheckBox.d.ts +53 -0
- package/dist/Components/Inputs/CheckBox.js +87 -0
- package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
- package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
- package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
- package/dist/Components/Inputs/ChoiceField.js +169 -0
- package/dist/Components/Inputs/DatePicker.d.ts +54 -0
- package/dist/Components/Inputs/DatePicker.js +246 -0
- package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
- package/dist/Components/Inputs/DateRangePicker.js +283 -0
- package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
- package/dist/Components/Inputs/DateTimePicker.js +400 -0
- package/dist/Components/Inputs/Dropdown.d.ts +117 -0
- package/dist/Components/Inputs/Dropdown.js +839 -0
- package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
- package/dist/Components/Inputs/DropdownParts.js +102 -0
- package/dist/Components/Inputs/EditorShell.d.ts +45 -0
- package/dist/Components/Inputs/EditorShell.js +214 -0
- package/dist/Components/Inputs/Field.d.ts +53 -0
- package/dist/Components/Inputs/Field.js +16 -0
- package/dist/Components/Inputs/GroupForm.d.ts +20 -0
- package/dist/Components/Inputs/GroupForm.js +172 -0
- package/dist/Components/Inputs/InputField.d.ts +59 -0
- package/dist/Components/Inputs/InputField.js +169 -0
- package/dist/Components/Inputs/InputRenderer.d.ts +37 -0
- package/dist/Components/Inputs/InputRenderer.js +99 -0
- package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
- package/dist/Components/Inputs/MarkdownEditor.js +289 -0
- package/dist/Components/Inputs/NumberBox.d.ts +53 -0
- package/dist/Components/Inputs/NumberBox.js +228 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
- package/dist/Components/Inputs/RadioGroup.js +43 -0
- package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
- package/dist/Components/Inputs/ReadOnlyNotice.js +117 -0
- package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
- package/dist/Components/Inputs/RichTextEditor.js +1170 -0
- package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
- package/dist/Components/Inputs/RichTextParts.js +157 -0
- package/dist/Components/Inputs/RowsInput.d.ts +115 -0
- package/dist/Components/Inputs/RowsInput.js +308 -0
- package/dist/Components/Inputs/Slider.d.ts +87 -0
- package/dist/Components/Inputs/Slider.js +216 -0
- package/dist/Components/Inputs/TabularForm.d.ts +27 -0
- package/dist/Components/Inputs/TabularForm.js +348 -0
- package/dist/Components/Inputs/TextArea.d.ts +43 -0
- package/dist/Components/Inputs/TextArea.js +31 -0
- package/dist/Components/Inputs/TextBox.d.ts +52 -0
- package/dist/Components/Inputs/TextBox.js +58 -0
- package/dist/Components/Inputs/TextField.d.ts +31 -0
- package/dist/Components/Inputs/TextField.js +35 -0
- package/dist/Components/Inputs/TimePanel.d.ts +34 -0
- package/dist/Components/Inputs/TimePanel.js +288 -0
- package/dist/Components/Inputs/TimePicker.d.ts +54 -0
- package/dist/Components/Inputs/TimePicker.js +240 -0
- package/dist/Components/Inputs/TreeView.d.ts +160 -0
- package/dist/Components/Inputs/TreeView.js +947 -0
- package/dist/Components/Layouts/MessageBox.d.ts +71 -0
- package/dist/Components/Layouts/MessageBox.js +104 -0
- package/dist/Components/Layouts/Popup.d.ts +92 -0
- package/dist/Components/Layouts/Popup.js +376 -0
- package/dist/Components/Layouts/PopupContext.d.ts +2 -0
- package/dist/Components/Layouts/PopupContext.js +4 -0
- package/dist/Components/Layouts/PopupDock.d.ts +22 -0
- package/dist/Components/Layouts/PopupDock.js +121 -0
- package/dist/Components/Layouts/PopupHost.d.ts +56 -0
- package/dist/Components/Layouts/PopupHost.js +163 -0
- package/dist/Components/Layouts/PopupRules.d.ts +30 -0
- package/dist/Components/Layouts/PopupRules.js +63 -0
- package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
- package/dist/Components/Layouts/ThemePicker.js +22 -0
- package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
- package/dist/Components/Layouts/ThemeScript.js +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.js +49 -0
- package/dist/Components/Layouts/Toaster.d.ts +76 -0
- package/dist/Components/Layouts/Toaster.js +172 -0
- package/dist/Components/Viewers/DataTable.d.ts +25 -0
- package/dist/Components/Viewers/DataTable.js +596 -0
- package/dist/Components/Viewers/DataTableColumns.d.ts +28 -0
- package/dist/Components/Viewers/DataTableColumns.js +243 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +138 -0
- package/dist/Components/Viewers/DataTableParts.js +157 -0
- package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
- package/dist/Components/Viewers/MarkdownView.js +79 -0
- package/dist/Components/Viewers/RichTextView.d.ts +18 -0
- package/dist/Components/Viewers/RichTextView.js +17 -0
- package/dist/Helpers/AnimationHelpers.d.ts +6 -0
- package/dist/Helpers/AnimationHelpers.js +35 -0
- package/dist/Helpers/ApiClient.d.ts +96 -0
- package/dist/Helpers/ApiClient.js +200 -0
- package/dist/Helpers/ApiFormHelpers.d.ts +49 -0
- package/dist/Helpers/ApiFormHelpers.js +120 -0
- package/dist/Helpers/ApiModules.d.ts +131 -0
- package/dist/Helpers/ApiModules.js +195 -0
- package/dist/Helpers/ApiResponses.d.ts +51 -0
- package/dist/Helpers/ApiResponses.js +250 -0
- package/dist/Helpers/ApiRoutes.d.ts +140 -0
- package/dist/Helpers/ApiRoutes.js +183 -0
- package/dist/Helpers/ApiToasts.d.ts +21 -0
- package/dist/Helpers/ApiToasts.js +22 -0
- package/dist/Helpers/ChatBackend.d.ts +134 -0
- package/dist/Helpers/ChatBackend.js +332 -0
- package/dist/Helpers/ChatHelpers.d.ts +189 -0
- package/dist/Helpers/ChatHelpers.js +486 -0
- package/dist/Helpers/ChatHooks.d.ts +81 -0
- package/dist/Helpers/ChatHooks.js +175 -0
- package/dist/Helpers/ChatStore.d.ts +132 -0
- package/dist/Helpers/ChatStore.js +394 -0
- package/dist/Helpers/ChatSummaries.d.ts +39 -0
- package/dist/Helpers/ChatSummaries.js +118 -0
- package/dist/Helpers/ClassHelpers.d.ts +3 -0
- package/dist/Helpers/ClassHelpers.js +4 -0
- package/dist/Helpers/CrudBackend.d.ts +98 -0
- package/dist/Helpers/CrudBackend.js +214 -0
- package/dist/Helpers/CrudHelpers.d.ts +111 -0
- package/dist/Helpers/CrudHelpers.js +267 -0
- package/dist/Helpers/DateHelpers.d.ts +88 -0
- package/dist/Helpers/DateHelpers.js +363 -0
- package/dist/Helpers/DatePreferences.d.ts +14 -0
- package/dist/Helpers/DatePreferences.js +94 -0
- package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
- package/dist/Helpers/DateTimeHelpers.js +286 -0
- package/dist/Helpers/DragHelpers.d.ts +129 -0
- package/dist/Helpers/DragHelpers.js +281 -0
- package/dist/Helpers/DropdownHelpers.d.ts +56 -0
- package/dist/Helpers/DropdownHelpers.js +180 -0
- package/dist/Helpers/ExcelBackend.d.ts +106 -0
- package/dist/Helpers/ExcelBackend.js +290 -0
- package/dist/Helpers/ExcelHelpers.d.ts +98 -0
- package/dist/Helpers/ExcelHelpers.js +302 -0
- package/dist/Helpers/FormDrafts.d.ts +18 -0
- package/dist/Helpers/FormDrafts.js +76 -0
- package/dist/Helpers/FormStore.d.ts +233 -0
- package/dist/Helpers/FormStore.js +752 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
- package/dist/Helpers/InputParamsHelpers.js +388 -0
- package/dist/Helpers/MarkdownEditing.d.ts +27 -0
- package/dist/Helpers/MarkdownEditing.js +250 -0
- package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
- package/dist/Helpers/MarkdownHelpers.js +775 -0
- package/dist/Helpers/MessageBuilder.d.ts +74 -0
- package/dist/Helpers/MessageBuilder.js +252 -0
- package/dist/Helpers/NepaliCalendar.d.ts +25 -0
- package/dist/Helpers/NepaliCalendar.js +107 -0
- package/dist/Helpers/NumberHelpers.d.ts +71 -0
- package/dist/Helpers/NumberHelpers.js +202 -0
- package/dist/Helpers/Permissions.d.ts +62 -0
- package/dist/Helpers/Permissions.js +133 -0
- package/dist/Helpers/PopoverHelpers.d.ts +68 -0
- package/dist/Helpers/PopoverHelpers.js +195 -0
- package/dist/Helpers/RichTextHelpers.d.ts +77 -0
- package/dist/Helpers/RichTextHelpers.js +1072 -0
- package/dist/Helpers/RowsStore.d.ts +166 -0
- package/dist/Helpers/RowsStore.js +606 -0
- package/dist/Helpers/TableColumns.d.ts +47 -0
- package/dist/Helpers/TableColumns.js +282 -0
- package/dist/Helpers/TableExport.d.ts +30 -0
- package/dist/Helpers/TableExport.js +97 -0
- package/dist/Helpers/TableHelpers.d.ts +110 -0
- package/dist/Helpers/TableHelpers.js +301 -0
- package/dist/Helpers/TableStore.d.ts +142 -0
- package/dist/Helpers/TableStore.js +530 -0
- package/dist/Helpers/ThemeHelpers.d.ts +15 -0
- package/dist/Helpers/ThemeHelpers.js +16 -0
- package/dist/Helpers/TimeHelpers.d.ts +73 -0
- package/dist/Helpers/TimeHelpers.js +253 -0
- package/dist/Helpers/TreeStore.d.ts +188 -0
- package/dist/Helpers/TreeStore.js +577 -0
- package/dist/Helpers/ValidationHelpers.d.ts +124 -0
- package/dist/Helpers/ValidationHelpers.js +301 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +174 -0
- package/dist/Interfaces/ApiInterfaces.js +30 -0
- package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
- package/{src/sso-client.tsx → dist/Interfaces/ChatInterfaces.js} +1 -1
- package/dist/Interfaces/CrudInterfaces.d.ts +232 -0
- package/dist/Interfaces/CrudInterfaces.js +1 -0
- package/dist/Interfaces/DateInterfaces.d.ts +154 -0
- package/dist/Interfaces/DateInterfaces.js +29 -0
- package/dist/Interfaces/ExcelInterfaces.d.ts +215 -0
- package/dist/Interfaces/ExcelInterfaces.js +45 -0
- package/dist/Interfaces/FormInterfaces.d.ts +402 -0
- package/dist/Interfaces/FormInterfaces.js +1 -0
- package/dist/Interfaces/InputInterfaces.d.ts +141 -0
- package/dist/Interfaces/InputInterfaces.js +1 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
- package/dist/Interfaces/MessageInterfaces.js +5 -0
- package/dist/Interfaces/TableInterfaces.d.ts +351 -0
- package/dist/Interfaces/TableInterfaces.js +1 -0
- package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
- package/dist/Interfaces/ThemeInterfaces.js +17 -0
- package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
- package/dist/Interfaces/TypeInterfaces.js +1 -0
- package/dist/Server.Index.d.ts +2 -0
- package/dist/Server.Index.js +5 -0
- package/dist/Services/ApiProxy.d.ts +32 -0
- package/dist/Services/ApiProxy.js +111 -0
- package/dist/Services/BrowserApi.d.ts +95 -0
- package/dist/Services/BrowserApi.js +129 -0
- package/dist/Services/ChatApi.d.ts +37 -0
- package/dist/Services/ChatApi.js +98 -0
- package/dist/Services/ExcelApi.d.ts +46 -0
- package/dist/Services/ExcelApi.js +196 -0
- package/dist/Services/ServerApi.d.ts +54 -0
- package/dist/Services/ServerApi.js +78 -0
- package/dist/Services/ThemeService.d.ts +17 -0
- package/dist/Services/ThemeService.js +83 -0
- package/dist/Shared.Index.d.ts +51 -0
- package/dist/Shared.Index.js +55 -0
- package/package.json +85 -33
- package/src/Styles/Nexus.Base.css +70 -0
- package/src/Styles/Nexus.Button.css +153 -0
- package/src/Styles/Nexus.Calendar.css +275 -0
- package/src/Styles/Nexus.Chat.css +1489 -0
- package/src/Styles/Nexus.Choice.css +345 -0
- package/src/Styles/Nexus.Crud.css +161 -0
- package/src/Styles/Nexus.DateTime.css +81 -0
- package/src/Styles/Nexus.Dropdown.css +774 -0
- package/src/Styles/Nexus.Editor.css +297 -0
- package/src/Styles/Nexus.Excel.css +370 -0
- package/src/Styles/Nexus.Form.css +321 -0
- package/src/Styles/Nexus.Index.css +36 -0
- package/src/Styles/Nexus.Input.css +637 -0
- package/src/Styles/Nexus.Markdown.css +265 -0
- package/src/Styles/Nexus.Menu.css +259 -0
- package/src/Styles/Nexus.Popup.css +731 -0
- package/src/Styles/Nexus.RichText.css +463 -0
- package/src/Styles/Nexus.Rows.css +596 -0
- package/src/Styles/Nexus.Slider.css +278 -0
- package/src/Styles/Nexus.Tab.Buttons.css +419 -0
- package/src/Styles/Nexus.Table.css +1296 -0
- package/src/Styles/Nexus.Theme.Picker.css +232 -0
- package/src/Styles/Nexus.Themes.css +194 -0
- package/src/Styles/Nexus.Time.css +147 -0
- package/src/Styles/Nexus.Toast.css +265 -0
- package/src/Styles/Nexus.Tokens.css +72 -0
- package/src/Styles/Nexus.Tree.css +435 -0
- package/src/api-services/authentication-service.tsx +0 -23
- package/src/api-services/preference-service.tsx +0 -5
- package/src/api-services/system-service.tsx +0 -29
- package/src/client.ts +0 -31
- package/src/components/documents/button.tsx +0 -133
- package/src/components/documents/icon-box.tsx +0 -93
- package/src/components/documents/page-title.tsx +0 -7
- package/src/components/documents/tab-button.tsx +0 -172
- package/src/components/documents/tag.tsx +0 -30
- package/src/components/index.js +0 -0
- package/src/components/inputs/checkbox-input.tsx +0 -66
- package/src/components/inputs/input-box.tsx +0 -48
- package/src/components/inputs/input-element.tsx +0 -65
- package/src/components/inputs/input-form.tsx +0 -164
- package/src/components/inputs/input.tsx +0 -181
- package/src/components/inputs/number-input.tsx +0 -108
- package/src/components/inputs/radiobox-input.tsx +0 -53
- package/src/components/inputs/textarea-input.tsx +0 -47
- package/src/components/inputs/textbox-input.tsx +0 -45
- package/src/components/layouts/global-dialogbox.tsx +0 -431
- package/src/components/layouts/global-layout.tsx +0 -64
- package/src/components/layouts/layout-helpers.tsx +0 -20
- package/src/components/layouts/panels/user-panel.tsx +0 -112
- package/src/components/layouts/utility-menu.tsx +0 -49
- package/src/components/panels/theme-panel.tsx +0 -46
- package/src/helpers/bitwise-helpers.tsx +0 -11
- package/src/helpers/browser-helpers.tsx +0 -156
- package/src/helpers/datasource-helpers.tsx +0 -99
- package/src/helpers/element-helpers.tsx +0 -123
- package/src/helpers/input-helpers.tsx +0 -74
- package/src/helpers/string-helpers.tsx +0 -28
- package/src/helpers/utility-helpers.tsx +0 -45
- package/src/helpers/validation-helpers.tsx +0 -302
- package/src/index.ts +0 -26
- package/src/interface.ts +0 -19
- package/src/interfaces/auth-token-interfaces.tsx +0 -6
- package/src/interfaces/browser-interfaces.tsx +0 -36
- package/src/interfaces/button-interfaces.tsx +0 -63
- package/src/interfaces/datasource-interfaces.tsx +0 -22
- package/src/interfaces/datatable-interfaces.tsx +0 -25
- package/src/interfaces/dialogbox-interfaces.tsx +0 -5
- package/src/interfaces/exception-interfaces.tsx +0 -99
- package/src/interfaces/http-interfaces.tsx +0 -129
- package/src/interfaces/icon-interfaces.tsx +0 -129
- package/src/interfaces/input-interfaces.tsx +0 -410
- package/src/interfaces/layout-interfaces.tsx +0 -191
- package/src/interfaces/menu-interfaces.tsx +0 -53
- package/src/interfaces/message-interfaces.tsx +0 -32
- package/src/interfaces/permission-interfaces.tsx +0 -9
- package/src/interfaces/storage-interfaces.tsx +0 -5
- package/src/interfaces/system-interfaces.tsx +0 -22
- package/src/interfaces/theme-interfaces.tsx +0 -123
- package/src/interfaces/type-interfaces.tsx +0 -28
- package/src/interfaces/user-interfaces.tsx +0 -47
- package/src/nexus-client.tsx +0 -21
- package/src/nexus.environments.tsx +0 -66
- package/src/proxy-api/proxy-backend.tsx +0 -60
- package/src/proxy-api/proxy-constants.tsx +0 -28
- package/src/proxy-api/proxy-helpers.tsx +0 -91
- package/src/proxy-api-client.tsx +0 -1
- package/src/proxy-api-server.tsx +0 -2
- package/src/services/http-client-services.tsx +0 -75
- package/src/services/http-services.tsx +0 -158
- package/src/services/loader-service.tsx +0 -185
- package/src/services/localstorage-service.tsx +0 -119
- package/src/services/message-services.tsx +0 -383
- package/src/services/theme-service.tsx +0 -163
- package/src/services/user-services.tsx +0 -10
- package/src/sso-config/auth-token-validation.ts +0 -16
- package/src/sso-config/callback-route.tsx +0 -35
- package/src/sso-config/cookie-encryption.ts +0 -51
- package/src/sso-config/cookie-helpers.tsx +0 -102
- package/src/sso-config/forgot-password-route.tsx +0 -36
- package/src/sso-config/oauth-callback-state.ts +0 -61
- package/src/sso-config/pkce-helpers.ts +0 -9
- package/src/sso-config/provider-complete-route.tsx +0 -18
- package/src/sso-config/redirect-context-actions.tsx +0 -45
- package/src/sso-config/redirect-context-helpers.tsx +0 -159
- package/src/sso-config/redirect-context-persist.tsx +0 -18
- package/src/sso-config/redirect-context-route.tsx +0 -45
- package/src/sso-config/refresh-route.tsx +0 -23
- package/src/sso-config/sign-in-route.tsx +0 -74
- package/src/sso-config/sign-out-route.tsx +0 -57
- package/src/sso-config/sign-up-route.tsx +0 -57
- package/src/sso-config/sso-backend-flow.ts +0 -38
- package/src/sso-config/sso-complete-route.tsx +0 -50
- package/src/sso-config/sso-config.ts +0 -59
- package/src/sso-config/sso-interfaces.tsx +0 -99
- package/src/sso-config/sso-response-helpers.tsx +0 -20
- package/src/sso-server.tsx +0 -21
- package/src/styles/nexus.animation.css +0 -269
- package/src/styles/nexus.core.css +0 -119
- package/src/styles/nexus.dialog.css +0 -144
- package/src/styles/nexus.grid-view.css +0 -42
- package/src/styles/nexus.icon.css +0 -51
- package/src/styles/nexus.input.css +0 -207
- package/src/styles/nexus.loader.css +0 -11
- package/src/styles/nexus.logic.css +0 -47
- package/src/styles/nexus.utility.css +0 -347
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
5
|
+
import { toDevanagariDigits } from "../../Helpers/DateHelpers.js";
|
|
6
|
+
import { formatTime, getMeridiemNames, isSecondsInRange, isTwelveHourFormat, listTimeSlots, normalizeTimeStep, nowIsoTime, SECONDS_PER_DAY, secondsToIsoTime, timeToSeconds, toIsoTime, } from "../../Helpers/TimeHelpers.js";
|
|
7
|
+
import { Button } from "../Documents/Button.js";
|
|
8
|
+
const PAGE = 5;
|
|
9
|
+
const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
|
|
10
|
+
const COLUMN_LABELS = { hour: "Hours", minute: "Minutes", second: "Seconds", meridiem: "AM or PM" };
|
|
11
|
+
/**
|
|
12
|
+
* The panel of a time picker: columns of hours, minutes, seconds, and AM/PM whose chosen items line up on the middle row, or
|
|
13
|
+
* a list of slots. Items outside `min` and `max`, off the `step`, or refused by `isTimeDisabled` are dimmed and cannot
|
|
14
|
+
* be picked. It holds no value of its own: the picker passes the value in and writes each pick to its box.
|
|
15
|
+
*/
|
|
16
|
+
export function TimePanel(props) {
|
|
17
|
+
const { value, onValueChange, onDone, view = "columns", format, script = "latin", seconds = false, min, max, isTimeDisabled, autoFocus = false, footer = true, className } = props;
|
|
18
|
+
const step = normalizeTimeStep(props.step);
|
|
19
|
+
const rootRef = useRef(null);
|
|
20
|
+
const scrolledRef = useRef(false);
|
|
21
|
+
const focusedRef = useRef(false);
|
|
22
|
+
const minIso = toIsoTime(min, true);
|
|
23
|
+
const maxIso = toIsoTime(max, true);
|
|
24
|
+
const from = minIso === null ? null : timeToSeconds(minIso);
|
|
25
|
+
const to = maxIso === null ? null : timeToSeconds(maxIso);
|
|
26
|
+
const devanagari = script === "devanagari";
|
|
27
|
+
const digits = (number) => (devanagari ? toDevanagariDigits(String(number).padStart(2, "0")) : String(number).padStart(2, "0"));
|
|
28
|
+
const allowed = (time) => isSecondsInRange(time, from, to) && time % step === 0 && !isTimeDisabled?.(secondsToIsoTime(time, seconds));
|
|
29
|
+
/** Whether any allowed time lies in a span of seconds, e.g. an hour. Stops at the first one. */
|
|
30
|
+
const anyAllowed = (start, end) => {
|
|
31
|
+
for (let time = Math.ceil(start / step) * step; time <= end; time += step)
|
|
32
|
+
if (allowed(time))
|
|
33
|
+
return true;
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
/** The first allowed time from a point, going forward through the day, or `null` when none is. */
|
|
37
|
+
const firstAllowedFrom = (start, end = start + SECONDS_PER_DAY - 1) => {
|
|
38
|
+
for (let time = Math.ceil(start / step) * step; time <= end; time += step) {
|
|
39
|
+
const inDay = time % SECONDS_PER_DAY;
|
|
40
|
+
if (allowed(inDay))
|
|
41
|
+
return inDay;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
const selected = value === null ? null : timeToSeconds(value);
|
|
46
|
+
// An empty field shows the columns around the first time it allows, with nothing chosen.
|
|
47
|
+
const base = useMemo(() => selected ?? firstAllowedFrom(from ?? 0) ?? 0, [selected, from, to, step, isTimeDisabled, seconds]);
|
|
48
|
+
function pick(kind, itemValue) {
|
|
49
|
+
let hour = Math.floor(base / 3600);
|
|
50
|
+
let minute = Math.floor(base / 60) % 60;
|
|
51
|
+
let second = seconds ? base % 60 : 0;
|
|
52
|
+
let start;
|
|
53
|
+
let end;
|
|
54
|
+
if (kind === "hour") {
|
|
55
|
+
hour = itemValue;
|
|
56
|
+
[start, end] = [hour * 3600, hour * 3600 + 3599];
|
|
57
|
+
}
|
|
58
|
+
else if (kind === "minute") {
|
|
59
|
+
minute = itemValue;
|
|
60
|
+
[start, end] = [hour * 3600 + minute * 60, hour * 3600 + minute * 60 + 59];
|
|
61
|
+
}
|
|
62
|
+
else if (kind === "second") {
|
|
63
|
+
second = itemValue;
|
|
64
|
+
start = end = hour * 3600 + minute * 60 + second;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
hour = (hour % 12) + itemValue * 12;
|
|
68
|
+
[start, end] = [itemValue * 43200, itemValue * 43200 + 43199];
|
|
69
|
+
}
|
|
70
|
+
let time = hour * 3600 + minute * 60 + second;
|
|
71
|
+
// The part just picked stays; smaller parts move to the first time it allows, e.g. 09:45 → 10 o'clock → 10:00 when 10:45 is past `max`.
|
|
72
|
+
if (!allowed(time))
|
|
73
|
+
time = firstAllowedFrom(start, end) ?? time;
|
|
74
|
+
if (time !== selected)
|
|
75
|
+
onValueChange(secondsToIsoTime(time, seconds));
|
|
76
|
+
}
|
|
77
|
+
// Columns
|
|
78
|
+
const twelveHour = isTwelveHourFormat(format);
|
|
79
|
+
const half = Math.floor(base / 43200);
|
|
80
|
+
const baseHour = Math.floor(base / 3600);
|
|
81
|
+
const baseMinute = Math.floor(base / 60) % 60;
|
|
82
|
+
const hourInterval = step % 3600 === 0 ? gcd(step / 3600, 24) : 1;
|
|
83
|
+
const minuteInterval = step % 60 === 0 ? gcd(step / 60, 60) : 1;
|
|
84
|
+
const secondInterval = gcd(step, 60);
|
|
85
|
+
const isChosen = selected !== null;
|
|
86
|
+
const columns = useMemo(() => {
|
|
87
|
+
if (view !== "columns")
|
|
88
|
+
return [];
|
|
89
|
+
const hours = [];
|
|
90
|
+
const hourStart = twelveHour ? half * 12 : 0;
|
|
91
|
+
for (let hour = hourStart; hour < hourStart + (twelveHour ? 12 : 24); hour++) {
|
|
92
|
+
if (hour % hourInterval !== 0)
|
|
93
|
+
continue;
|
|
94
|
+
hours.push({
|
|
95
|
+
value: hour,
|
|
96
|
+
label: digits(twelveHour ? hour % 12 || 12 : hour),
|
|
97
|
+
selected: isChosen && hour === baseHour,
|
|
98
|
+
disabled: !anyAllowed(hour * 3600, hour * 3600 + 3599),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const minutes = [];
|
|
102
|
+
for (let minute = 0; minute < 60; minute += minuteInterval) {
|
|
103
|
+
const start = baseHour * 3600 + minute * 60;
|
|
104
|
+
minutes.push({
|
|
105
|
+
value: minute,
|
|
106
|
+
label: digits(minute),
|
|
107
|
+
selected: isChosen && minute === baseMinute,
|
|
108
|
+
disabled: seconds ? !anyAllowed(start, start + 59) : !allowed(start),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const result = [
|
|
112
|
+
{ kind: "hour", items: hours },
|
|
113
|
+
{ kind: "minute", items: minutes },
|
|
114
|
+
];
|
|
115
|
+
if (seconds) {
|
|
116
|
+
const items = [];
|
|
117
|
+
for (let second = 0; second < 60; second += secondInterval) {
|
|
118
|
+
items.push({ value: second, label: digits(second), selected: isChosen && second === base % 60, disabled: !allowed(baseHour * 3600 + baseMinute * 60 + second) });
|
|
119
|
+
}
|
|
120
|
+
result.push({ kind: "second", items });
|
|
121
|
+
}
|
|
122
|
+
if (twelveHour) {
|
|
123
|
+
result.push({
|
|
124
|
+
kind: "meridiem",
|
|
125
|
+
items: getMeridiemNames(script).map((label, index) => ({
|
|
126
|
+
value: index,
|
|
127
|
+
label,
|
|
128
|
+
selected: isChosen && index === half,
|
|
129
|
+
disabled: !anyAllowed(index * 43200, index * 43200 + 43199),
|
|
130
|
+
})),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}, [view, base, isChosen, twelveHour, seconds, script, step, from, to, isTimeDisabled]);
|
|
135
|
+
// List
|
|
136
|
+
const slotStep = step >= 300 ? step : 1800;
|
|
137
|
+
const slots = useMemo(() => {
|
|
138
|
+
if (view !== "list")
|
|
139
|
+
return [];
|
|
140
|
+
return listTimeSlots(slotStep, minIso, maxIso).map(iso => {
|
|
141
|
+
const time = timeToSeconds(iso);
|
|
142
|
+
return { value: time, iso, label: formatTime(iso, { format, script }), disabled: !(time % step === 0 && !isTimeDisabled?.(secondsToIsoTime(time, seconds))) };
|
|
143
|
+
});
|
|
144
|
+
}, [view, slotStep, minIso, maxIso, format, script, step, isTimeDisabled, seconds]);
|
|
145
|
+
// The list's slot to scroll to and focus first: the chosen one, the one just before the chosen time, or the first free one.
|
|
146
|
+
const listAnchor = useMemo(() => {
|
|
147
|
+
if (view !== "list" || slots.length === 0)
|
|
148
|
+
return null;
|
|
149
|
+
if (selected !== null) {
|
|
150
|
+
const exact = slots.find(slot => slot.value === selected);
|
|
151
|
+
if (exact)
|
|
152
|
+
return exact.value;
|
|
153
|
+
const before = slots.filter(slot => slot.value <= selected).at(-1);
|
|
154
|
+
if (before)
|
|
155
|
+
return before.value;
|
|
156
|
+
}
|
|
157
|
+
return (slots.find(slot => !slot.disabled) ?? slots[0]).value;
|
|
158
|
+
}, [view, slots, selected]);
|
|
159
|
+
// Chosen items scroll to the middle row of their column, so they line up across columns with their neighbours above
|
|
160
|
+
// and below; the list centers its slot the same way. At once when the panel opens, smoothly after that, unless the
|
|
161
|
+
// user asked for reduced motion. (Columns are positioned, so an item's offsetTop is measured from the column.)
|
|
162
|
+
// With `autoFocus`, focus then moves in once.
|
|
163
|
+
useLayoutEffect(() => {
|
|
164
|
+
const root = rootRef.current;
|
|
165
|
+
if (!root)
|
|
166
|
+
return;
|
|
167
|
+
const align = () => {
|
|
168
|
+
const smooth = scrolledRef.current && !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
169
|
+
for (const column of root.querySelectorAll("[data-column]")) {
|
|
170
|
+
const target = column.querySelector(view === "list" ? "[data-anchor]" : '[aria-selected="true"]');
|
|
171
|
+
if (!target)
|
|
172
|
+
continue;
|
|
173
|
+
const top = target.offsetTop - (column.clientHeight - target.offsetHeight) / 2;
|
|
174
|
+
if (Math.abs(column.scrollTop - top) > 1)
|
|
175
|
+
column.scrollTo({ top, behavior: smooth ? "smooth" : "instant" });
|
|
176
|
+
}
|
|
177
|
+
scrolledRef.current = true;
|
|
178
|
+
if (autoFocus && !focusedRef.current) {
|
|
179
|
+
focusedRef.current = true;
|
|
180
|
+
root.querySelector('[role="option"][tabindex="0"]')?.focus({ preventScroll: true });
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
if (root.getClientRects().length > 0) {
|
|
184
|
+
align();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// On the first render the popover around the panel is still hidden (the picker shows it after this effect), and a
|
|
188
|
+
// hidden element can neither scroll nor take focus. The next frame comes before the first paint.
|
|
189
|
+
const frame = requestAnimationFrame(align);
|
|
190
|
+
return () => cancelAnimationFrame(frame);
|
|
191
|
+
});
|
|
192
|
+
function onColumnKeyDown(event, kind, items) {
|
|
193
|
+
const target = event.target.closest("[data-value]");
|
|
194
|
+
if (!target)
|
|
195
|
+
return;
|
|
196
|
+
const index = items.findIndex(item => String(item.value) === target.dataset.value);
|
|
197
|
+
const enabled = (from, direction) => {
|
|
198
|
+
for (let i = from; i >= 0 && i < items.length; i += direction)
|
|
199
|
+
if (!items[i].disabled)
|
|
200
|
+
return i;
|
|
201
|
+
return -1;
|
|
202
|
+
};
|
|
203
|
+
let next = -1;
|
|
204
|
+
switch (event.key) {
|
|
205
|
+
case "ArrowDown":
|
|
206
|
+
next = enabled(index + 1, 1);
|
|
207
|
+
break;
|
|
208
|
+
case "ArrowUp":
|
|
209
|
+
next = enabled(index - 1, -1);
|
|
210
|
+
break;
|
|
211
|
+
case "PageDown":
|
|
212
|
+
next = enabled(Math.min(index + PAGE, items.length - 1), 1);
|
|
213
|
+
if (next < 0)
|
|
214
|
+
next = enabled(items.length - 1, -1);
|
|
215
|
+
break;
|
|
216
|
+
case "PageUp":
|
|
217
|
+
next = enabled(Math.max(index - PAGE, 0), -1);
|
|
218
|
+
if (next < 0)
|
|
219
|
+
next = enabled(0, 1);
|
|
220
|
+
break;
|
|
221
|
+
case "Home":
|
|
222
|
+
next = enabled(0, 1);
|
|
223
|
+
break;
|
|
224
|
+
case "End":
|
|
225
|
+
next = enabled(items.length - 1, -1);
|
|
226
|
+
break;
|
|
227
|
+
case "ArrowLeft":
|
|
228
|
+
case "ArrowRight": {
|
|
229
|
+
if (kind === "slot")
|
|
230
|
+
return;
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
const all = [...(rootRef.current?.querySelectorAll("[data-column]") ?? [])];
|
|
233
|
+
const column = all[all.indexOf(target.closest("[data-column]")) + (event.key === "ArrowRight" ? 1 : -1)];
|
|
234
|
+
column?.querySelector('[tabindex="0"]')?.focus({ preventScroll: true });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
case "Enter":
|
|
238
|
+
case " ":
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
if (kind === "slot") {
|
|
241
|
+
const slot = items[index];
|
|
242
|
+
if (slot && !slot.disabled)
|
|
243
|
+
choose(slot.value);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
if (event.key === " " && index >= 0 && !items[index].disabled)
|
|
247
|
+
pick(kind, items[index].value);
|
|
248
|
+
else
|
|
249
|
+
onDone();
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
default:
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
if (next < 0 || next === index)
|
|
257
|
+
return;
|
|
258
|
+
const item = items[next];
|
|
259
|
+
// Moving within a column keeps its items, so the element to focus is already there.
|
|
260
|
+
const element = target.parentElement?.querySelector(`[data-value="${item.value}"]`);
|
|
261
|
+
element?.focus({ preventScroll: true });
|
|
262
|
+
if (kind === "slot") {
|
|
263
|
+
// The list moves focus only; Enter or a click picks.
|
|
264
|
+
element?.scrollIntoView({ block: "nearest" });
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// A column is a spin button: the value follows focus, and the column scrolls it to the chosen row.
|
|
268
|
+
pick(kind, item.value);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function choose(time) {
|
|
272
|
+
if (time !== selected)
|
|
273
|
+
onValueChange(secondsToIsoTime(time, seconds));
|
|
274
|
+
onDone();
|
|
275
|
+
}
|
|
276
|
+
const now = useMemo(() => {
|
|
277
|
+
const time = Math.floor(timeToSeconds(nowIsoTime(true)) / step) * step;
|
|
278
|
+
return allowed(time) ? time : null;
|
|
279
|
+
// Read when the panel renders; a panel stays open for seconds, not hours.
|
|
280
|
+
}, [step, from, to, isTimeDisabled, seconds]);
|
|
281
|
+
if (view === "list") {
|
|
282
|
+
return (_jsx("div", { ref: rootRef, className: cx("nx-time", "nx-time--list", className), lang: devanagari ? "ne" : undefined, children: _jsxs("div", { role: "listbox", "aria-label": "Times", "data-column": "slot", className: "nx-time__list", tabIndex: -1, onKeyDown: event => onColumnKeyDown(event, "slot", slots), children: [slots.map(slot => (_jsx("div", { role: "option", "data-value": slot.value, "data-anchor": slot.value === listAnchor || undefined, "aria-selected": slot.value === selected, "aria-disabled": slot.disabled || undefined, tabIndex: slot.value === listAnchor ? 0 : -1, className: "nx-time__item nx-time__slot", onClick: () => !slot.disabled && choose(slot.value), children: slot.label }, slot.value))), slots.length === 0 && _jsx("div", { className: "nx-time__empty", children: "No times available" })] }) }));
|
|
283
|
+
}
|
|
284
|
+
return (_jsxs("div", { ref: rootRef, className: cx("nx-time", className), lang: devanagari ? "ne" : undefined, children: [_jsx("div", { className: "nx-time__columns", children: columns.map(({ kind, items }) => {
|
|
285
|
+
const reachable = items.find(item => item.selected && !item.disabled) ?? items.find(item => !item.disabled) ?? items[0];
|
|
286
|
+
return (_jsx("div", { role: "listbox", "aria-label": COLUMN_LABELS[kind], "data-column": kind, className: cx("nx-time__column", kind === "meridiem" && "nx-time__column--meridiem"), tabIndex: -1, onKeyDown: event => onColumnKeyDown(event, kind, items), children: items.map((item, index) => (_jsx("div", { role: "option", "data-value": item.value, "aria-selected": item.selected, "aria-disabled": item.disabled || undefined, tabIndex: item === reachable ? 0 : -1, className: "nx-time__item", onClick: () => !item.disabled && pick(kind, item.value), children: item.label }, kind === "hour" ? index : item.value))) }, kind));
|
|
287
|
+
}) }), footer && (_jsxs("div", { className: "nx-time__foot", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: now === null, onClick: () => now !== null && choose(now), children: "Now" }), _jsx(Button, { size: "sm", variant: "primary", onClick: onDone, children: "Done" })] }))] }));
|
|
288
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ChangeEvent, type ComponentPropsWithRef, type ReactNode } from "react";
|
|
2
|
+
import type { DateScript, TimeInput, TimeRules } from "../../Interfaces/DateInterfaces.ts";
|
|
3
|
+
import type { InputDesign, ValidateOn } from "../../Interfaces/InputInterfaces.ts";
|
|
4
|
+
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
5
|
+
import { type TimePanelView } from "./TimePanel.tsx";
|
|
6
|
+
type NativeInputProps = Omit<ComponentPropsWithRef<"input">, "type" | "size" | "value" | "defaultValue" | "min" | "max" | "step" | "prefix" | "pattern" | "minLength" | "maxLength" | "children" | keyof TimeRules>;
|
|
7
|
+
export interface TimePickerProps extends NativeInputProps, TimeRules {
|
|
8
|
+
label?: ReactNode;
|
|
9
|
+
/** Help text under the box. */
|
|
10
|
+
hint?: ReactNode;
|
|
11
|
+
/** An error from outside, such as a server message. Shown before the field's own message. */
|
|
12
|
+
error?: string | null;
|
|
13
|
+
/** Default `standard`. */
|
|
14
|
+
design?: InputDesign;
|
|
15
|
+
/** Default `md`. */
|
|
16
|
+
size?: Size;
|
|
17
|
+
/** Decorative icon at the start. The clock button is always at the end. */
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
/** Text before the value inside the box. */
|
|
20
|
+
prefix?: ReactNode;
|
|
21
|
+
/** How the time is written: `hh:mm A` (02:30 PM), `HH:mm` (14:30), ... Seconds are added when `step` needs them. Default: the user's setting. */
|
|
22
|
+
format?: string;
|
|
23
|
+
/** `devanagari` writes Nepali digits and पूर्वाह्न/अपराह्न. Default: Devanagari when the user's settings show Nepali dates in Devanagari. */
|
|
24
|
+
script?: DateScript;
|
|
25
|
+
/** `columns` (default): hours, minutes, and AM/PM, for any time. `list`: a slot every `step` (30 minutes for steps under 5), for bookings. */
|
|
26
|
+
view?: TimePanelView;
|
|
27
|
+
/** Times that cannot be picked, such as booked slots. Receives the ISO time. */
|
|
28
|
+
isTimeDisabled?: (iso: string) => boolean;
|
|
29
|
+
/** Shows a clear button while there is a value. Default true. */
|
|
30
|
+
clearable?: boolean;
|
|
31
|
+
/** What a read-only field says when someone tries to change it. Default "Read-only: this time cannot be changed." */
|
|
32
|
+
readOnlyMessage?: string;
|
|
33
|
+
/** Name used in messages, e.g. "Arrival is required." Default: the label, when it is text. */
|
|
34
|
+
fieldName?: string;
|
|
35
|
+
/** When the field shows its own message. Default `blur`; submitting a form always shows it. */
|
|
36
|
+
validateOn?: ValidateOn;
|
|
37
|
+
/** The time as an ISO string ("14:30"), or `null` for empty. A `Date` or an ISO date-time is accepted and read as its local time. */
|
|
38
|
+
value?: TimeInput;
|
|
39
|
+
defaultValue?: TimeInput;
|
|
40
|
+
/** After each change: the ISO time ("HH:mm", or "HH:mm:ss" when `step` has seconds), or `null` when the box is empty or holds text that is not a time. */
|
|
41
|
+
onValueChange?: (value: string | null, event: ChangeEvent<HTMLInputElement>) => void;
|
|
42
|
+
/** Class of the outer field. `inputClassName` goes on the input. */
|
|
43
|
+
className?: string;
|
|
44
|
+
inputClassName?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A time input: type a time on either clock ("2:30 pm", "1430"), or pick one from the panel that opens under the box.
|
|
48
|
+
* Written in the user's time format, 12- or 24-hour, and always held as an ISO time with no date and no zone, which a
|
|
49
|
+
* form sends through a hidden input with the field's `name`. Uncontrolled by default, so typing re-renders nothing.
|
|
50
|
+
*
|
|
51
|
+
* @example <TimePicker name="arrival" label="Arrival" min="12:00" max="22:00" step={1800} view="list" />
|
|
52
|
+
*/
|
|
53
|
+
export declare function TimePicker(props: TimePickerProps): import("react").JSX.Element;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { iconClock, iconX, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { usesDevanagari } from "../../Helpers/DateHelpers.js";
|
|
7
|
+
import { useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
8
|
+
import { followAnchor, isPopoverOpen, placeUnderAnchor } from "../../Helpers/PopoverHelpers.js";
|
|
9
|
+
import { formatTime, normalizeTimeStep, parseTimeText, timeFormatWithSeconds, toIsoTime } from "../../Helpers/TimeHelpers.js";
|
|
10
|
+
import { DEFAULT_FIELD_NAME, validateTime } from "../../Helpers/ValidationHelpers.js";
|
|
11
|
+
import { Field, hasContent } from "./Field.js";
|
|
12
|
+
import { setInputValue, useInputField } from "./InputField.js";
|
|
13
|
+
import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
|
|
14
|
+
import { TimePanel } from "./TimePanel.js";
|
|
15
|
+
/**
|
|
16
|
+
* A time input: type a time on either clock ("2:30 pm", "1430"), or pick one from the panel that opens under the box.
|
|
17
|
+
* Written in the user's time format, 12- or 24-hour, and always held as an ISO time with no date and no zone, which a
|
|
18
|
+
* form sends through a hidden input with the field's `name`. Uncontrolled by default, so typing re-renders nothing.
|
|
19
|
+
*
|
|
20
|
+
* @example <TimePicker name="arrival" label="Arrival" min="12:00" max="22:00" step={1800} view="list" />
|
|
21
|
+
*/
|
|
22
|
+
export function TimePicker(props) {
|
|
23
|
+
const { label, hint, error, design = "standard", size = "md", icon, prefix, view = "columns", clearable = true, fieldName, readOnlyMessage = "Read-only: this time cannot be changed.", validateOn, value, defaultValue, onValueChange, isTimeDisabled, className, inputClassName, style, required, min, max, step, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onClick, ...inputProps } = props;
|
|
24
|
+
const preferences = useDatePreferences();
|
|
25
|
+
const script = props.script ?? (usesDevanagari(preferences.calendar, preferences.script) ? "devanagari" : "latin");
|
|
26
|
+
const seconds = normalizeTimeStep(step) % 60 !== 0;
|
|
27
|
+
const format = timeFormatWithSeconds(props.format ?? preferences.timeFormat, seconds);
|
|
28
|
+
const editable = !disabled && !readOnly;
|
|
29
|
+
const initial = toIsoTime(value !== undefined ? value : defaultValue, seconds);
|
|
30
|
+
const text = (iso) => formatTime(iso, { format, script });
|
|
31
|
+
const messageName = fieldName ?? (typeof label === "string" ? label : DEFAULT_FIELD_NAME);
|
|
32
|
+
const valueRef = useRef(initial);
|
|
33
|
+
const editedRef = useRef(false);
|
|
34
|
+
const hiddenRef = useRef(null);
|
|
35
|
+
const panelRef = useRef(null);
|
|
36
|
+
const focusPanelRef = useRef(false);
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
// Re-renders the panel's selection while it is open; typing with it closed re-renders nothing.
|
|
39
|
+
const [panelValue, setPanelValue] = useState(initial);
|
|
40
|
+
const rules = {
|
|
41
|
+
required,
|
|
42
|
+
min,
|
|
43
|
+
max,
|
|
44
|
+
step,
|
|
45
|
+
validate: iso => (iso && isTimeDisabled?.(iso) ? `${messageName} cannot be ${text(iso)}.` : validate?.(iso)),
|
|
46
|
+
};
|
|
47
|
+
const field = useInputField({
|
|
48
|
+
...props,
|
|
49
|
+
onChange: event => {
|
|
50
|
+
const input = event.currentTarget;
|
|
51
|
+
const iso = input.value.trim() ? parseTimeText(input.value, { seconds }) : null;
|
|
52
|
+
const changed = iso !== valueRef.current;
|
|
53
|
+
editedRef.current = true;
|
|
54
|
+
valueRef.current = iso;
|
|
55
|
+
sync(iso);
|
|
56
|
+
if (open)
|
|
57
|
+
setPanelValue(iso);
|
|
58
|
+
onChange?.(event);
|
|
59
|
+
if (changed)
|
|
60
|
+
onValueChange?.(iso, event);
|
|
61
|
+
},
|
|
62
|
+
}, {
|
|
63
|
+
// Text that is not a time is checked as itself, so the message says what to type.
|
|
64
|
+
check: (input, messageName) => validateTime(valueRef.current ?? (input.value.trim() ? input.value : null), rules, messageName, text),
|
|
65
|
+
checkKeys: [value, required, min, max, step, validate, isTimeDisabled, format, script],
|
|
66
|
+
onReset: input => {
|
|
67
|
+
// Back to the time the field started with, written in the current format.
|
|
68
|
+
editedRef.current = false;
|
|
69
|
+
valueRef.current = initial;
|
|
70
|
+
input.value = text(initial);
|
|
71
|
+
sync(initial);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
// A read-only box has no panel: a lock, and a note when someone tries to open or change it, say why.
|
|
75
|
+
const readOnlyNotice = useReadOnlyNotice(Boolean(readOnly) && !disabled, field.elementRef, readOnlyMessage, { keys: ["ArrowDown"] });
|
|
76
|
+
/** Writes what depends on the value straight to the DOM: the hidden input a form sends. */
|
|
77
|
+
function sync(iso) {
|
|
78
|
+
if (hiddenRef.current)
|
|
79
|
+
hiddenRef.current.value = iso ?? "";
|
|
80
|
+
}
|
|
81
|
+
// A controlled value that differs from the box replaces its text. So does a default value that arrives after
|
|
82
|
+
// mount (from an API, say), as long as the user has not typed anything yet.
|
|
83
|
+
useLayoutEffect(() => {
|
|
84
|
+
const input = field.elementRef.current;
|
|
85
|
+
if (!input || (value === undefined && editedRef.current) || initial === valueRef.current)
|
|
86
|
+
return;
|
|
87
|
+
valueRef.current = initial;
|
|
88
|
+
input.value = text(initial);
|
|
89
|
+
sync(initial);
|
|
90
|
+
setPanelValue(initial);
|
|
91
|
+
}, [initial]);
|
|
92
|
+
// A new format or script rewrites the same time.
|
|
93
|
+
useLayoutEffect(() => {
|
|
94
|
+
const input = field.elementRef.current;
|
|
95
|
+
if (input && valueRef.current)
|
|
96
|
+
input.value = text(valueRef.current);
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
|
+
}, [format, script]);
|
|
99
|
+
// React sets a hidden input's default value again on every render, and for a hidden input that is its value.
|
|
100
|
+
// After each render, before paint, the hidden input takes the box's time back.
|
|
101
|
+
useLayoutEffect(() => {
|
|
102
|
+
sync(valueRef.current);
|
|
103
|
+
});
|
|
104
|
+
// The panel is a manual popover under the box (above it when there is no room), shown once its content has rendered.
|
|
105
|
+
// It closes only through this component (Esc, Enter, Done, a pick in the list, focus or a press elsewhere), so the
|
|
106
|
+
// state always matches the screen: a click right after Esc opens it again, and a click on the box keeps it open.
|
|
107
|
+
useLayoutEffect(() => {
|
|
108
|
+
const panel = panelRef.current;
|
|
109
|
+
const box = field.elementRef.current?.closest(".nx-field__box");
|
|
110
|
+
if (!open || !panel || !box)
|
|
111
|
+
return;
|
|
112
|
+
if (!isPopoverOpen(panel))
|
|
113
|
+
panel.showPopover();
|
|
114
|
+
// No arrow: the panel opens flush under the box and is read as the field's own list.
|
|
115
|
+
const place = () => placeUnderAnchor(panel, box.getBoundingClientRect(), { arrow: false });
|
|
116
|
+
place();
|
|
117
|
+
const stopFollowing = followAnchor(place);
|
|
118
|
+
const onPointerDown = (event) => {
|
|
119
|
+
const target = event.target;
|
|
120
|
+
if (!panel.contains(target) && !box.contains(target))
|
|
121
|
+
closePanel();
|
|
122
|
+
};
|
|
123
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
124
|
+
return () => {
|
|
125
|
+
stopFollowing();
|
|
126
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
127
|
+
if (isPopoverOpen(panel))
|
|
128
|
+
panel.hidePopover();
|
|
129
|
+
};
|
|
130
|
+
}, [open]);
|
|
131
|
+
function openPanel(focusPanel) {
|
|
132
|
+
if (!editable)
|
|
133
|
+
return;
|
|
134
|
+
// The box is the control: it keeps focus while the panel is open, so Esc and Enter reach it.
|
|
135
|
+
field.elementRef.current?.focus();
|
|
136
|
+
focusPanelRef.current = focusPanel;
|
|
137
|
+
// Shown before its content renders, so the panel can scroll its columns and take focus as it mounts. Nothing is
|
|
138
|
+
// painted in between: the render that fills it runs before the frame.
|
|
139
|
+
const panel = panelRef.current;
|
|
140
|
+
if (panel && !isPopoverOpen(panel))
|
|
141
|
+
panel.showPopover();
|
|
142
|
+
setPanelValue(valueRef.current);
|
|
143
|
+
setOpen(true);
|
|
144
|
+
}
|
|
145
|
+
/** Closes the panel. With `returnFocus`, focus that was inside the panel goes back to the box (Esc, Enter, Done). */
|
|
146
|
+
function closePanel(returnFocus = false) {
|
|
147
|
+
const panel = panelRef.current;
|
|
148
|
+
const focusInPanel = Boolean(panel?.contains(document.activeElement));
|
|
149
|
+
// Blur, a press elsewhere, and Done may all close it; hiding happens once.
|
|
150
|
+
if (isPopoverOpen(panel))
|
|
151
|
+
panel.hidePopover();
|
|
152
|
+
setOpen(false);
|
|
153
|
+
if (returnFocus && focusInPanel)
|
|
154
|
+
field.elementRef.current?.focus();
|
|
155
|
+
}
|
|
156
|
+
function select(iso) {
|
|
157
|
+
const input = field.elementRef.current;
|
|
158
|
+
if (!input)
|
|
159
|
+
return;
|
|
160
|
+
setInputValue(input, text(iso));
|
|
161
|
+
field.touch();
|
|
162
|
+
}
|
|
163
|
+
function clear() {
|
|
164
|
+
const input = field.elementRef.current;
|
|
165
|
+
if (!input)
|
|
166
|
+
return;
|
|
167
|
+
setInputValue(input, "");
|
|
168
|
+
input.focus();
|
|
169
|
+
}
|
|
170
|
+
function handleKeyDown(event) {
|
|
171
|
+
const { key } = event;
|
|
172
|
+
readOnlyNotice.onKeyDown(event);
|
|
173
|
+
if (editable && key === "ArrowDown") {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
if (open)
|
|
176
|
+
panelRef.current?.querySelector('[role="option"][tabindex="0"]')?.focus({ preventScroll: true });
|
|
177
|
+
else
|
|
178
|
+
openPanel(true);
|
|
179
|
+
}
|
|
180
|
+
else if (key === "Escape" && open) {
|
|
181
|
+
// Closes the panel only: the page (a dialog, say) does not see this Esc.
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
event.stopPropagation();
|
|
184
|
+
closePanel();
|
|
185
|
+
}
|
|
186
|
+
else if (clearable && editable && key === "Escape" && event.currentTarget.value) {
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
event.stopPropagation();
|
|
189
|
+
clear();
|
|
190
|
+
}
|
|
191
|
+
else if (key === "Enter" && open) {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
closePanel();
|
|
194
|
+
}
|
|
195
|
+
else if (key === "Tab" && open) {
|
|
196
|
+
// Tab moves on to the next field, not into the panel; ↓ is the way in.
|
|
197
|
+
closePanel();
|
|
198
|
+
}
|
|
199
|
+
onKeyDown?.(event);
|
|
200
|
+
}
|
|
201
|
+
function handleClick(event) {
|
|
202
|
+
if (!open)
|
|
203
|
+
openPanel(false);
|
|
204
|
+
readOnlyNotice.onClick(event);
|
|
205
|
+
onClick?.(event);
|
|
206
|
+
}
|
|
207
|
+
function handleBlur(event) {
|
|
208
|
+
// Typed text takes the canonical form once it is a time.
|
|
209
|
+
const input = event.currentTarget;
|
|
210
|
+
if (valueRef.current && input.value !== text(valueRef.current))
|
|
211
|
+
input.value = text(valueRef.current);
|
|
212
|
+
// Focus moving anywhere but the panel closes it.
|
|
213
|
+
if (open && !(event.relatedTarget instanceof Node && panelRef.current?.contains(event.relatedTarget)))
|
|
214
|
+
closePanel();
|
|
215
|
+
readOnlyNotice.hide();
|
|
216
|
+
field.controlProps.onBlur(event);
|
|
217
|
+
}
|
|
218
|
+
// Focus leaving the panel for anything but the box (Tab past Done, a click elsewhere) closes it.
|
|
219
|
+
function handlePanelBlur(event) {
|
|
220
|
+
const next = event.relatedTarget;
|
|
221
|
+
if (next instanceof Node && (event.currentTarget.contains(next) || next === field.elementRef.current))
|
|
222
|
+
return;
|
|
223
|
+
closePanel();
|
|
224
|
+
}
|
|
225
|
+
function handlePanelKeyDown(event) {
|
|
226
|
+
if (event.key !== "Escape")
|
|
227
|
+
return;
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
event.stopPropagation();
|
|
230
|
+
closePanel(true);
|
|
231
|
+
}
|
|
232
|
+
const showClear = clearable && editable;
|
|
233
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, design: design, size: size, hint: hint, error: field.error, icon: icon, prefix: prefix, actions: _jsxs(_Fragment, { children: [showClear && (_jsx("button", { type: "button", className: "nx-field__action nx-field__action--clear", tabIndex: -1, "aria-label": "Clear", title: "Clear", onMouseDown: event => event.preventDefault(), onClick: clear, children: _jsx(InlineIcon, { icon: iconX }) })), readOnlyNotice.lock, !readOnly && (_jsx("button", { type: "button", className: "nx-field__action nx-field__action--clock", tabIndex: -1, "aria-label": "Choose a time", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? `${field.controlId}-times` : undefined, title: "Choose a time", disabled: disabled, onMouseDown: event => event.preventDefault(), onClick: () => (open ? closePanel() : openPanel(false)), children: _jsx(InlineIcon, { icon: iconClock }) }))] }), disabled: disabled, readOnly: readOnly, raiseLabel: Boolean(placeholder) || hasContent(prefix), className: cx("nx-field--time", className), style: style, children: [_jsx("input", { autoComplete: "off", spellCheck: false, ...inputProps, ...field.controlProps, type: "text", name: undefined, form: form, lang: script === "devanagari" ? "ne" : undefined, defaultValue: text(initial), className: cx("nx-field__control", "nx-field__control--time", inputClassName), placeholder: placeholder || " ", disabled: disabled, readOnly: readOnly, onBlur: handleBlur, onFocus: onFocus, onKeyDown: handleKeyDown, onClick: handleClick }), name && _jsx("input", { ref: hiddenRef, type: "hidden", name: name, form: form, disabled: disabled, defaultValue: initial ?? "" })] }), readOnlyNotice.notice, !readOnly && (_jsx("div", { ref: panelRef, id: `${field.controlId}-times`, popover: "manual", role: "dialog", "aria-label": "Choose a time", className: cx("nx-popover", "nx-time-popover"), onKeyDown: handlePanelKeyDown, onBlur: handlePanelBlur,
|
|
234
|
+
// The box keeps focus while items, buttons, and the space between them are clicked. A press on a column
|
|
235
|
+
// itself is left alone, so its scroll bar can be dragged; the column takes focus, inside the panel.
|
|
236
|
+
onMouseDown: event => {
|
|
237
|
+
if (!event.target.matches("[data-column]"))
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
}, children: open && (_jsx(TimePanel, { view: view, value: panelValue, onValueChange: select, onDone: () => closePanel(true), format: format, script: script, seconds: seconds, step: step, min: min, max: max, isTimeDisabled: isTimeDisabled, autoFocus: focusPanelRef.current })) }))] }));
|
|
240
|
+
}
|