nexus-shared 1.1.20 → 2.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/LICENSE +21 -0
- package/README.md +903 -0
- package/dist/Client.Index.d.ts +44 -0
- package/dist/Client.Index.js +49 -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 +238 -0
- package/dist/Components/Documents/SplitButton.d.ts +64 -0
- package/dist/Components/Documents/SplitButton.js +127 -0
- package/dist/Components/Documents/TabButtons.d.ts +62 -0
- package/dist/Components/Documents/TabButtons.js +19 -0
- package/dist/Components/Forms/ApiForm.d.ts +21 -0
- package/dist/Components/Forms/ApiForm.js +99 -0
- package/dist/Components/Forms/Crud.d.ts +17 -0
- package/dist/Components/Forms/Crud.js +556 -0
- package/dist/Components/Forms/CrudParts.d.ts +15 -0
- package/dist/Components/Forms/CrudParts.js +36 -0
- package/dist/Components/Forms/Form.d.ts +52 -0
- package/dist/Components/Forms/Form.js +319 -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 +399 -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 +170 -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 +35 -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 +116 -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 +104 -0
- package/dist/Components/Inputs/RowsInput.js +259 -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 +23 -0
- package/dist/Components/Inputs/TabularForm.js +283 -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 +239 -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 +29 -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 +542 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +134 -0
- package/dist/Components/Viewers/DataTableParts.js +145 -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 +74 -0
- package/dist/Helpers/ApiClient.js +197 -0
- package/dist/Helpers/ApiFormHelpers.d.ts +26 -0
- package/dist/Helpers/ApiFormHelpers.js +56 -0
- package/dist/Helpers/ApiModules.d.ts +135 -0
- package/dist/Helpers/ApiModules.js +194 -0
- package/dist/Helpers/ApiResponses.d.ts +41 -0
- package/dist/Helpers/ApiResponses.js +159 -0
- package/dist/Helpers/ApiRoutes.d.ts +150 -0
- package/dist/Helpers/ApiRoutes.js +191 -0
- package/dist/Helpers/ApiToasts.d.ts +21 -0
- package/dist/Helpers/ApiToasts.js +22 -0
- package/dist/Helpers/ClassHelpers.d.ts +3 -0
- package/dist/Helpers/ClassHelpers.js +4 -0
- package/dist/Helpers/CrudBackend.d.ts +56 -0
- package/dist/Helpers/CrudBackend.js +119 -0
- package/dist/Helpers/CrudHelpers.d.ts +76 -0
- package/dist/Helpers/CrudHelpers.js +202 -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/FormDrafts.d.ts +18 -0
- package/dist/Helpers/FormDrafts.js +76 -0
- package/dist/Helpers/FormStore.d.ts +231 -0
- package/dist/Helpers/FormStore.js +734 -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 +254 -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 +43 -0
- package/dist/Helpers/PopoverHelpers.js +91 -0
- package/dist/Helpers/RichTextHelpers.d.ts +77 -0
- package/dist/Helpers/RichTextHelpers.js +1072 -0
- package/dist/Helpers/RowsStore.d.ts +160 -0
- package/dist/Helpers/RowsStore.js +584 -0
- package/dist/Helpers/TableColumns.d.ts +40 -0
- package/dist/Helpers/TableColumns.js +254 -0
- package/dist/Helpers/TableExport.d.ts +30 -0
- package/dist/Helpers/TableExport.js +97 -0
- package/dist/Helpers/TableHelpers.d.ts +102 -0
- package/dist/Helpers/TableHelpers.js +281 -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 +149 -0
- package/dist/Interfaces/ApiInterfaces.js +24 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +171 -0
- package/{src/sso-client.tsx → dist/Interfaces/CrudInterfaces.js} +1 -1
- package/dist/Interfaces/DateInterfaces.d.ts +154 -0
- package/dist/Interfaces/DateInterfaces.js +29 -0
- package/dist/Interfaces/FormInterfaces.d.ts +352 -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 +291 -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 +78 -0
- package/dist/Services/BrowserApi.js +111 -0
- package/dist/Services/ServerApi.d.ts +49 -0
- package/dist/Services/ServerApi.js +75 -0
- package/dist/Services/ThemeService.d.ts +17 -0
- package/dist/Services/ThemeService.js +83 -0
- package/dist/Shared.Index.d.ts +43 -0
- package/dist/Shared.Index.js +47 -0
- package/package.json +82 -33
- package/src/Styles/Nexus.Base.css +70 -0
- package/src/Styles/Nexus.Button.css +151 -0
- package/src/Styles/Nexus.Calendar.css +275 -0
- package/src/Styles/Nexus.Choice.css +345 -0
- package/src/Styles/Nexus.Crud.css +84 -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.Form.css +298 -0
- package/src/Styles/Nexus.Index.css +34 -0
- package/src/Styles/Nexus.Input.css +637 -0
- package/src/Styles/Nexus.Markdown.css +265 -0
- package/src/Styles/Nexus.Menu.css +249 -0
- package/src/Styles/Nexus.Popup.css +731 -0
- package/src/Styles/Nexus.RichText.css +463 -0
- package/src/Styles/Nexus.Rows.css +566 -0
- package/src/Styles/Nexus.Slider.css +278 -0
- package/src/Styles/Nexus.Tab.Buttons.css +364 -0
- package/src/Styles/Nexus.Table.css +1014 -0
- package/src/Styles/Nexus.Theme.Picker.css +159 -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 -34
- package/src/client.ts +0 -31
- package/src/components/documents/button.tsx +0 -142
- 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 -59
- 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 -57
- 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 -49
- 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 -161
- 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.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,202 @@
|
|
|
1
|
+
/** Default locale for number text. Fixed rather than the browser's, so server-rendered text matches the browser's. */
|
|
2
|
+
export const DEFAULT_NUMBER_LOCALE = "en-US";
|
|
3
|
+
/** Most digits a number field takes. A JavaScript number holds 15 significant digits exactly; beyond that, digits change. */
|
|
4
|
+
export const MAX_NUMBER_DIGITS = 15;
|
|
5
|
+
const symbolsByLocale = new Map();
|
|
6
|
+
const formatters = new Map();
|
|
7
|
+
// Direction marks some locales put around a minus sign; they are invisible and never part of the number.
|
|
8
|
+
const BIDI_MARKS = /[\u061c\u200e\u200f]/g;
|
|
9
|
+
// U+2212, the typographic minus sign some locales use.
|
|
10
|
+
const MINUS_SIGN = "\u2212";
|
|
11
|
+
const SPACES = /\s/g;
|
|
12
|
+
/** The decimal and group separators of a locale. */
|
|
13
|
+
export function getNumberSymbols(locale = DEFAULT_NUMBER_LOCALE) {
|
|
14
|
+
let symbols = symbolsByLocale.get(locale);
|
|
15
|
+
if (!symbols) {
|
|
16
|
+
const parts = new Intl.NumberFormat(locale).formatToParts(1234567.5);
|
|
17
|
+
symbols = {
|
|
18
|
+
decimal: parts.find(part => part.type === "decimal")?.value ?? ".",
|
|
19
|
+
group: parts.find(part => part.type === "group")?.value ?? ",",
|
|
20
|
+
};
|
|
21
|
+
symbolsByLocale.set(locale, symbols);
|
|
22
|
+
}
|
|
23
|
+
return symbols;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Formats a number for display: `formatNumber(1250, { decimals: 2, fixedDecimals: true, grouping: true })` gives "1,250.00".
|
|
27
|
+
* Returns "" for `null`, `undefined`, and `NaN`. Digits are always 0–9, and the minus sign is "-", so the text can be typed back.
|
|
28
|
+
*/
|
|
29
|
+
export function formatNumber(value, options = {}) {
|
|
30
|
+
if (value === null || value === undefined || !Number.isFinite(value))
|
|
31
|
+
return "";
|
|
32
|
+
const { decimals, fixedDecimals = false, grouping = false, locale = DEFAULT_NUMBER_LOCALE } = options;
|
|
33
|
+
const maxDecimals = decimals ?? MAX_NUMBER_DIGITS;
|
|
34
|
+
const minDecimals = fixedDecimals ? maxDecimals : 0;
|
|
35
|
+
const key = `${locale}|${minDecimals}|${maxDecimals}|${grouping}`;
|
|
36
|
+
let formatter = formatters.get(key);
|
|
37
|
+
if (!formatter) {
|
|
38
|
+
formatter = new Intl.NumberFormat(locale, {
|
|
39
|
+
numberingSystem: "latn",
|
|
40
|
+
useGrouping: grouping,
|
|
41
|
+
minimumFractionDigits: minDecimals,
|
|
42
|
+
maximumFractionDigits: maxDecimals,
|
|
43
|
+
});
|
|
44
|
+
formatters.set(key, formatter);
|
|
45
|
+
}
|
|
46
|
+
// Rounded first, the same way as roundDecimal, which also turns -0.001 into 0 rather than "-0".
|
|
47
|
+
return formatter.format(roundDecimal(value, maxDecimals)).replace(BIDI_MARKS, "").replace(MINUS_SIGN, "-");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reads a number written in the locale's format: "1,250.50" gives 1250.5, and in `de-DE` "1.250,50" does.
|
|
51
|
+
* Returns `null` for empty text (also a lone minus sign or decimal separator) and `NaN` for text that is not a number.
|
|
52
|
+
*/
|
|
53
|
+
export function parseNumber(text, locale = DEFAULT_NUMBER_LOCALE) {
|
|
54
|
+
const { decimal, group } = getNumberSymbols(locale);
|
|
55
|
+
let clean = (text ?? "").replace(BIDI_MARKS, "").replace(SPACES, "").replace(MINUS_SIGN, "-");
|
|
56
|
+
if (group.trim())
|
|
57
|
+
clean = clean.replaceAll(group, "");
|
|
58
|
+
if (decimal !== ".")
|
|
59
|
+
clean = clean.replace(decimal, ".");
|
|
60
|
+
if (/^-?\.?$/.test(clean))
|
|
61
|
+
return null;
|
|
62
|
+
if (!/^-?(?:\d+\.?\d*|\.\d+)$/.test(clean))
|
|
63
|
+
return Number.NaN;
|
|
64
|
+
return Number(clean) || 0;
|
|
65
|
+
}
|
|
66
|
+
/** Counts the digits in text, to keep numbers within `MAX_NUMBER_DIGITS`. */
|
|
67
|
+
export function countDigits(text) {
|
|
68
|
+
let count = 0;
|
|
69
|
+
for (const char of text)
|
|
70
|
+
if (char >= "0" && char <= "9")
|
|
71
|
+
count++;
|
|
72
|
+
return count;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether text typed so far can become a valid number: "", "-", "12.", and ".5" can; "1.2.3" and "12a" cannot.
|
|
76
|
+
* The text is edit text: digits, at most one decimal separator, and a leading minus sign, with no group separators.
|
|
77
|
+
*/
|
|
78
|
+
export function isPartialNumber(text, decimal, rules) {
|
|
79
|
+
let index = 0;
|
|
80
|
+
if (text.startsWith("-")) {
|
|
81
|
+
if (!rules.negative)
|
|
82
|
+
return false;
|
|
83
|
+
index = 1;
|
|
84
|
+
}
|
|
85
|
+
let digits = 0;
|
|
86
|
+
let fraction = -1;
|
|
87
|
+
for (; index < text.length; index++) {
|
|
88
|
+
const char = text.charAt(index);
|
|
89
|
+
if (char >= "0" && char <= "9") {
|
|
90
|
+
digits++;
|
|
91
|
+
if (fraction >= 0)
|
|
92
|
+
fraction++;
|
|
93
|
+
}
|
|
94
|
+
else if (char === decimal && fraction < 0 && rules.decimals !== 0) {
|
|
95
|
+
fraction = 0;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return digits <= MAX_NUMBER_DIGITS && (rules.decimals === undefined || fraction <= rules.decimals);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Removes what cannot be part of a number from text that reached the field unchecked (an input method, autofill, a drop),
|
|
105
|
+
* keeping the caret where it was relative to the text that stays. Digits are never removed.
|
|
106
|
+
*/
|
|
107
|
+
export function cleanNumberText(text, caret, decimal, rules) {
|
|
108
|
+
let output = "";
|
|
109
|
+
let nextCaret = caret;
|
|
110
|
+
let hasDecimal = false;
|
|
111
|
+
for (let index = 0; index < text.length; index++) {
|
|
112
|
+
const char = text.charAt(index) === MINUS_SIGN ? "-" : text.charAt(index);
|
|
113
|
+
const keep = (char >= "0" && char <= "9") ||
|
|
114
|
+
(char === "-" && output === "" && rules.negative) ||
|
|
115
|
+
(char === decimal && !hasDecimal && rules.decimals !== 0);
|
|
116
|
+
if (keep) {
|
|
117
|
+
output += char;
|
|
118
|
+
if (char === decimal)
|
|
119
|
+
hasDecimal = true;
|
|
120
|
+
}
|
|
121
|
+
else if (index < caret) {
|
|
122
|
+
nextCaret--;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { text: output, caret: nextCaret };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Takes the number out of pasted text, such as "Rs 1,250.50", "1 250,50 €", or "-12%", and returns it as edit text
|
|
129
|
+
* ("1250.50"). Returns `null` when the text holds no single clear number, so a paste never turns into a wrong value.
|
|
130
|
+
*/
|
|
131
|
+
export function readPastedNumber(text, locale = DEFAULT_NUMBER_LOCALE) {
|
|
132
|
+
const { decimal, group } = getNumberSymbols(locale);
|
|
133
|
+
let clean = text.replace(BIDI_MARKS, "").replace(SPACES, "").replaceAll(MINUS_SIGN, "-");
|
|
134
|
+
if (group.trim())
|
|
135
|
+
clean = clean.replaceAll(group, "");
|
|
136
|
+
const first = clean.search(/\d/);
|
|
137
|
+
if (first < 0)
|
|
138
|
+
return null;
|
|
139
|
+
let start = first;
|
|
140
|
+
// A decimal separator before the first digit belongs to the number (".5"), unless it ends a word ("Rs.1250").
|
|
141
|
+
if (clean[start - 1] === decimal && !/[\p{L}.]/u.test(clean[start - 2] ?? ""))
|
|
142
|
+
start--;
|
|
143
|
+
if (clean[start - 1] === "-")
|
|
144
|
+
start--;
|
|
145
|
+
const last = clean.search(/\d\D*$/) + 1;
|
|
146
|
+
const number = clean.slice(start, last);
|
|
147
|
+
const valid = new RegExp(`^-?(?:\\d+(?:${escapeRegExp(decimal)}\\d*)?|${escapeRegExp(decimal)}\\d+)$`);
|
|
148
|
+
return valid.test(number) ? number : null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Rounds to a number of decimal places the way people do: 1.005 → 1.01, and -2.5 → -3.
|
|
152
|
+
* Works on the number's shortest decimal text, so binary fractions such as 1.005 (stored as 1.00499…) round up.
|
|
153
|
+
*/
|
|
154
|
+
export function roundDecimal(value, digits) {
|
|
155
|
+
if (!Number.isFinite(value))
|
|
156
|
+
return value;
|
|
157
|
+
const [mantissa, exponent = "0"] = String(Math.abs(value)).split("e");
|
|
158
|
+
const shifted = Math.round(Number(`${mantissa}e${Number(exponent) + digits}`));
|
|
159
|
+
const rounded = Number(`${shifted}e${Number(exponent) - digits}`);
|
|
160
|
+
if (Number.isNaN(rounded))
|
|
161
|
+
return value;
|
|
162
|
+
return (value < 0 ? -rounded : rounded) || 0;
|
|
163
|
+
}
|
|
164
|
+
/** Keeps a value within `min` and `max`, each optional. */
|
|
165
|
+
export function clampNumber(value, min, max) {
|
|
166
|
+
if (min !== undefined && value < min)
|
|
167
|
+
return min;
|
|
168
|
+
if (max !== undefined && value > max)
|
|
169
|
+
return max;
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
/** Digits after the decimal point of a number: 0.25 → 2, 1e-7 → 7. */
|
|
173
|
+
export function countDecimals(value) {
|
|
174
|
+
const match = /(?:\.(\d+))?(?:e([+-]\d+))?$/.exec(String(value));
|
|
175
|
+
return Math.max(0, (match?.[1]?.length ?? 0) - Number(match?.[2] ?? 0));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The next value for an arrow key or a +/− button. Values move along the grid of `step` from `min` (or 0), so 2.3 with
|
|
179
|
+
* step 1 goes up to 3, and stay within `min` and `max`. An empty field starts at 0, or at the limit nearest to 0.
|
|
180
|
+
*/
|
|
181
|
+
export function stepNumber(value, direction, options = {}) {
|
|
182
|
+
const { min, max, decimals } = options;
|
|
183
|
+
const size = Math.abs(options.step ?? 1) || 1;
|
|
184
|
+
if (value === null || Number.isNaN(value)) {
|
|
185
|
+
const start = clampNumber(0, min, max);
|
|
186
|
+
if (start !== 0)
|
|
187
|
+
return start;
|
|
188
|
+
value = 0;
|
|
189
|
+
}
|
|
190
|
+
const base = min ?? 0;
|
|
191
|
+
const precision = Math.max(countDecimals(size), countDecimals(value), countDecimals(base));
|
|
192
|
+
// Rounded first, so float noise (0.30000000000000004 steps) does not skip or repeat a step.
|
|
193
|
+
const offset = roundDecimal((value - base) / size, 9);
|
|
194
|
+
const index = direction > 0 ? Math.floor(offset) + 1 : Math.ceil(offset) - 1;
|
|
195
|
+
let next = clampNumber(roundDecimal(base + index * size, precision), min, max);
|
|
196
|
+
if (decimals !== undefined)
|
|
197
|
+
next = roundDecimal(next, decimals);
|
|
198
|
+
return next;
|
|
199
|
+
}
|
|
200
|
+
function escapeRegExp(text) {
|
|
201
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
202
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ApiEndpoint, ApiModuleValue } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
/** An app's permission rules. Subclasses answer `can`, and call `changed()` when the user's rights change. */
|
|
3
|
+
export declare abstract class Permissions {
|
|
4
|
+
/** Whether the user may call this endpoint. */
|
|
5
|
+
abstract can(endpoint: ApiEndpoint): boolean;
|
|
6
|
+
/** Whether the rules are known yet. While they load, components show placeholders rather than "not allowed". */
|
|
7
|
+
get ready(): boolean;
|
|
8
|
+
/** Tells the page the rules changed (a sign-in, another branch), so buttons and lists show or hide. */
|
|
9
|
+
protected changed(): void;
|
|
10
|
+
}
|
|
11
|
+
/** Everyone may do everything. The rules until an app registers its own. */
|
|
12
|
+
export declare class AllowAllPermissions extends Permissions {
|
|
13
|
+
can(): boolean;
|
|
14
|
+
}
|
|
15
|
+
/** One action a Nexus user may call, as the backend sends them (`AccessActions`, or the layout's `accessActions`). */
|
|
16
|
+
export interface NexusAccessAction {
|
|
17
|
+
controllerName: string;
|
|
18
|
+
actionName: string;
|
|
19
|
+
/** The backend's `EHttpMethods` number (Get 0, Put 1, Delete 2, Post 3, Patch 6), or the method's name. */
|
|
20
|
+
httpMethod: number | string;
|
|
21
|
+
/** The modules the action belongs to: `NexusModule` flags, combined. */
|
|
22
|
+
modules: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The controller and the action of an endpoint's path, as a Nexus backend names them: "Room/Detail/{id}" is the
|
|
26
|
+
* `Detail` action of the `Room` controller. Values in braces are left out.
|
|
27
|
+
*/
|
|
28
|
+
export declare function endpointAction(path: string): {
|
|
29
|
+
controller: string;
|
|
30
|
+
action: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The Nexus rules: the user may call an endpoint when their access actions hold its controller, action, and method,
|
|
34
|
+
* for its module. Endpoints of the app's own routes (`NexusModule.None`) are always allowed.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const permissions = definePermissions(new NexusPermissions());
|
|
38
|
+
* permissions.setActions(layout.accessActions); // after sign-in, from the backend
|
|
39
|
+
*/
|
|
40
|
+
export declare class NexusPermissions extends Permissions {
|
|
41
|
+
private actions;
|
|
42
|
+
/** `actions`: the user's access actions, or `null` while they load. */
|
|
43
|
+
constructor(actions?: readonly NexusAccessAction[] | null);
|
|
44
|
+
get ready(): boolean;
|
|
45
|
+
/** Replaces the user's access actions, such as after sign-in or a switch of branch. `null`: not known yet. */
|
|
46
|
+
setActions(actions: readonly NexusAccessAction[] | null): void;
|
|
47
|
+
private index;
|
|
48
|
+
can(endpoint: ApiEndpoint<ApiModuleValue>): boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Registers the app's permission rules, once, in a module the root layout loads (the same place as its API modules).
|
|
52
|
+
* Returns them, so the app can keep a handle to update them after sign-in.
|
|
53
|
+
*/
|
|
54
|
+
export declare function definePermissions<T extends Permissions>(permissions: T): T;
|
|
55
|
+
/** The app's permission rules: the registered ones, or `AllowAllPermissions`. */
|
|
56
|
+
export declare function getPermissions(): Permissions;
|
|
57
|
+
/** Whether the user may call this endpoint, by the app's rules. */
|
|
58
|
+
export declare function canCall(endpoint: ApiEndpoint | null | undefined): boolean;
|
|
59
|
+
/** For `useSyncExternalStore`: listens for new rules or changed rights. */
|
|
60
|
+
export declare function subscribePermissions(listener: () => void): () => void;
|
|
61
|
+
/** For `useSyncExternalStore`: a number that changes whenever the rules do. */
|
|
62
|
+
export declare const permissionsVersion: () => number;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Permissions: may the user call this endpoint? Components ask before they show a button or a list (the CRUD's Add,
|
|
3
|
+
* Delete, and its Trash list), so a user sees only what they may do; the server still checks every call.
|
|
4
|
+
*
|
|
5
|
+
* `Permissions` is the interface an app implements, for its own backend's rules; `NexusPermissions` reads the Nexus
|
|
6
|
+
* backend's access actions. Register one with `definePermissions`. Until then everything is allowed, which suits an
|
|
7
|
+
* app without sign-in.
|
|
8
|
+
*/
|
|
9
|
+
/** An app's permission rules. Subclasses answer `can`, and call `changed()` when the user's rights change. */
|
|
10
|
+
export class Permissions {
|
|
11
|
+
/** Whether the rules are known yet. While they load, components show placeholders rather than "not allowed". */
|
|
12
|
+
get ready() {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
/** Tells the page the rules changed (a sign-in, another branch), so buttons and lists show or hide. */
|
|
16
|
+
changed() {
|
|
17
|
+
if (current === this)
|
|
18
|
+
notify();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Everyone may do everything. The rules until an app registers its own. */
|
|
22
|
+
export class AllowAllPermissions extends Permissions {
|
|
23
|
+
can() {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const NEXUS_METHODS = { GET: 0, PUT: 1, DELETE: 2, POST: 3, PATCH: 6 };
|
|
28
|
+
function methodName(method) {
|
|
29
|
+
if (typeof method === "string")
|
|
30
|
+
return method.toUpperCase();
|
|
31
|
+
return Object.keys(NEXUS_METHODS).find(name => NEXUS_METHODS[name] === method);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The controller and the action of an endpoint's path, as a Nexus backend names them: "Room/Detail/{id}" is the
|
|
35
|
+
* `Detail` action of the `Room` controller. Values in braces are left out.
|
|
36
|
+
*/
|
|
37
|
+
export function endpointAction(path) {
|
|
38
|
+
const parts = path.split("/").filter(part => part && !part.startsWith("{"));
|
|
39
|
+
const action = parts.pop() ?? "";
|
|
40
|
+
return { controller: parts.join("/"), action };
|
|
41
|
+
}
|
|
42
|
+
const actionKey = (controller, action, method) => `${controller.toLowerCase()}|${action.toLowerCase()}|${method}`;
|
|
43
|
+
/**
|
|
44
|
+
* The Nexus rules: the user may call an endpoint when their access actions hold its controller, action, and method,
|
|
45
|
+
* for its module. Endpoints of the app's own routes (`NexusModule.None`) are always allowed.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const permissions = definePermissions(new NexusPermissions());
|
|
49
|
+
* permissions.setActions(layout.accessActions); // after sign-in, from the backend
|
|
50
|
+
*/
|
|
51
|
+
export class NexusPermissions extends Permissions {
|
|
52
|
+
actions = null;
|
|
53
|
+
/** `actions`: the user's access actions, or `null` while they load. */
|
|
54
|
+
constructor(actions = null) {
|
|
55
|
+
super();
|
|
56
|
+
if (actions)
|
|
57
|
+
this.index(actions);
|
|
58
|
+
}
|
|
59
|
+
get ready() {
|
|
60
|
+
return this.actions !== null;
|
|
61
|
+
}
|
|
62
|
+
/** Replaces the user's access actions, such as after sign-in or a switch of branch. `null`: not known yet. */
|
|
63
|
+
setActions(actions) {
|
|
64
|
+
if (actions)
|
|
65
|
+
this.index(actions);
|
|
66
|
+
else
|
|
67
|
+
this.actions = null;
|
|
68
|
+
this.changed();
|
|
69
|
+
}
|
|
70
|
+
index(actions) {
|
|
71
|
+
const map = new Map();
|
|
72
|
+
for (const item of actions) {
|
|
73
|
+
const method = methodName(item.httpMethod);
|
|
74
|
+
if (!method || !item.controllerName || !item.actionName)
|
|
75
|
+
continue;
|
|
76
|
+
const key = actionKey(item.controllerName, item.actionName, method);
|
|
77
|
+
const list = map.get(key);
|
|
78
|
+
if (list)
|
|
79
|
+
list.push(Number(item.modules) || 0);
|
|
80
|
+
else
|
|
81
|
+
map.set(key, [Number(item.modules) || 0]);
|
|
82
|
+
}
|
|
83
|
+
this.actions = map;
|
|
84
|
+
}
|
|
85
|
+
// Any module value, whichever type the app registers: a number is read as flags, text as it is.
|
|
86
|
+
can(endpoint) {
|
|
87
|
+
if (endpoint.module === 0)
|
|
88
|
+
return true;
|
|
89
|
+
if (!this.actions)
|
|
90
|
+
return false;
|
|
91
|
+
const { controller, action } = endpointAction(endpoint.path);
|
|
92
|
+
const modules = this.actions.get(actionKey(controller, action, endpoint.method));
|
|
93
|
+
if (!modules)
|
|
94
|
+
return false;
|
|
95
|
+
const module = endpoint.module;
|
|
96
|
+
// An action's modules are flags: it belongs to every module whose bit is set.
|
|
97
|
+
return modules.some(flags => (typeof module === "number" ? (flags & module) === module : String(flags) === String(module)));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
let current = new AllowAllPermissions();
|
|
101
|
+
let version = 0;
|
|
102
|
+
const listeners = new Set();
|
|
103
|
+
function notify() {
|
|
104
|
+
version++;
|
|
105
|
+
for (const listener of listeners)
|
|
106
|
+
listener();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Registers the app's permission rules, once, in a module the root layout loads (the same place as its API modules).
|
|
110
|
+
* Returns them, so the app can keep a handle to update them after sign-in.
|
|
111
|
+
*/
|
|
112
|
+
export function definePermissions(permissions) {
|
|
113
|
+
current = permissions;
|
|
114
|
+
notify();
|
|
115
|
+
return permissions;
|
|
116
|
+
}
|
|
117
|
+
/** The app's permission rules: the registered ones, or `AllowAllPermissions`. */
|
|
118
|
+
export function getPermissions() {
|
|
119
|
+
return current;
|
|
120
|
+
}
|
|
121
|
+
/** Whether the user may call this endpoint, by the app's rules. */
|
|
122
|
+
export function canCall(endpoint) {
|
|
123
|
+
return Boolean(endpoint) && current.can(endpoint);
|
|
124
|
+
}
|
|
125
|
+
/** For `useSyncExternalStore`: listens for new rules or changed rights. */
|
|
126
|
+
export function subscribePermissions(listener) {
|
|
127
|
+
listeners.add(listener);
|
|
128
|
+
return () => {
|
|
129
|
+
listeners.delete(listener);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** For `useSyncExternalStore`: a number that changes whenever the rules do. */
|
|
133
|
+
export const permissionsVersion = () => version;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type PopoverAlign = "start" | "end";
|
|
2
|
+
export interface PlaceUnderAnchorOptions {
|
|
3
|
+
/** Which edge of the anchor the panel lines up with. Default `start` (the left edge). */
|
|
4
|
+
align?: PopoverAlign;
|
|
5
|
+
/** Space between the anchor and the panel, in pixels. Default 4. */
|
|
6
|
+
gap?: number;
|
|
7
|
+
/** Space kept from the window's edges, in pixels. Default 8. */
|
|
8
|
+
margin?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Places a panel under an anchor, or above it when there is no room below, aligned to the anchor's start or end
|
|
12
|
+
* edge and kept inside the window. Returns whether the panel went above.
|
|
13
|
+
*/
|
|
14
|
+
export declare function placeUnderAnchor(panel: HTMLElement, anchor: DOMRect, options?: PlaceUnderAnchorOptions): boolean;
|
|
15
|
+
/** Which side of its anchor a list panel opened on. */
|
|
16
|
+
export type PanelSide = "below" | "above";
|
|
17
|
+
export interface PlaceListPanelOptions {
|
|
18
|
+
/** The side of an earlier call, kept while it has `minHeight` of room, so a list that loads more items does not jump across its anchor. */
|
|
19
|
+
side?: PanelSide;
|
|
20
|
+
/** The tallest the panel may be, in pixels. Default 360. */
|
|
21
|
+
maxHeight?: number;
|
|
22
|
+
/** A side is used while at least this much room is there, in pixels; with less, the other side is used when it has more. Default 180. */
|
|
23
|
+
minHeight?: number;
|
|
24
|
+
/** Space between the anchor and the panel, in pixels. Default 4. */
|
|
25
|
+
gap?: number;
|
|
26
|
+
/** Space kept from the window's edges, in pixels. Default 8. */
|
|
27
|
+
margin?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Places a list panel under its anchor, at least as wide as the anchor, or above it when the room below is short and
|
|
31
|
+
* there is more above. The panel's height is capped to the room on its side, so a long list scrolls inside the window
|
|
32
|
+
* instead of running past it. Returns the side, to pass back while the panel stays open.
|
|
33
|
+
*/
|
|
34
|
+
export declare function placeListPanel(panel: HTMLElement, anchor: DOMRect, options?: PlaceListPanelOptions): PanelSide;
|
|
35
|
+
/**
|
|
36
|
+
* Places a panel at a point, such as where the user right-clicked: to the right of and below the point when there
|
|
37
|
+
* is room, otherwise to the left of or above it, and never past the window's edges.
|
|
38
|
+
*/
|
|
39
|
+
export declare function placeAtPoint(panel: HTMLElement, x: number, y: number, margin?: number): void;
|
|
40
|
+
/** Runs `place` again, once per frame at most, while the page scrolls or the window changes size. Returns the stop function. */
|
|
41
|
+
export declare function followAnchor(place: () => void): () => void;
|
|
42
|
+
/** Whether the popover is shown. Safe on browsers without popover support, where it is never "open". */
|
|
43
|
+
export declare function isPopoverOpen(panel: HTMLElement | null): boolean;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Placing native popovers. A panel is a fixed [popover] element; these helpers set its top and left once it is shown,
|
|
3
|
+
* keep it inside the window, and move it again while the page scrolls. They measure offsetWidth and offsetHeight,
|
|
4
|
+
* which ignore the panel's opening transform.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Places a panel under an anchor, or above it when there is no room below, aligned to the anchor's start or end
|
|
8
|
+
* edge and kept inside the window. Returns whether the panel went above.
|
|
9
|
+
*/
|
|
10
|
+
export function placeUnderAnchor(panel, anchor, options = {}) {
|
|
11
|
+
const { align = "start", gap = 4, margin = 8 } = options;
|
|
12
|
+
const width = panel.offsetWidth;
|
|
13
|
+
const height = panel.offsetHeight;
|
|
14
|
+
const below = anchor.bottom + gap;
|
|
15
|
+
const above = anchor.top - gap - height;
|
|
16
|
+
const fitsBelow = below + height <= window.innerHeight - margin;
|
|
17
|
+
const top = fitsBelow || above < margin ? below : above;
|
|
18
|
+
const wanted = align === "end" ? anchor.right - width : anchor.left;
|
|
19
|
+
const left = Math.max(margin, Math.min(wanted, window.innerWidth - width - margin));
|
|
20
|
+
panel.style.top = `${Math.round(top)}px`;
|
|
21
|
+
panel.style.left = `${Math.round(left)}px`;
|
|
22
|
+
return top !== below;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Places a list panel under its anchor, at least as wide as the anchor, or above it when the room below is short and
|
|
26
|
+
* there is more above. The panel's height is capped to the room on its side, so a long list scrolls inside the window
|
|
27
|
+
* instead of running past it. Returns the side, to pass back while the panel stays open.
|
|
28
|
+
*/
|
|
29
|
+
export function placeListPanel(panel, anchor, options = {}) {
|
|
30
|
+
const { maxHeight = 360, minHeight = 180, gap = 4, margin = 8 } = options;
|
|
31
|
+
const roomBelow = window.innerHeight - anchor.bottom - gap - margin;
|
|
32
|
+
const roomAbove = anchor.top - gap - margin;
|
|
33
|
+
const enough = Math.min(minHeight, maxHeight);
|
|
34
|
+
let side = options.side ?? (roomBelow >= enough || roomBelow >= roomAbove ? "below" : "above");
|
|
35
|
+
// An earlier side is kept while it has room. When the anchor grows (chips wrapping in a dropdown's box) or the page
|
|
36
|
+
// moves until that side has less than minHeight, and the other side has more, the panel moves across instead of
|
|
37
|
+
// shrinking to nothing.
|
|
38
|
+
const room = side === "below" ? roomBelow : roomAbove;
|
|
39
|
+
const otherRoom = side === "below" ? roomAbove : roomBelow;
|
|
40
|
+
if (room < enough && otherRoom > room)
|
|
41
|
+
side = side === "below" ? "above" : "below";
|
|
42
|
+
// Runs after every render of an open list: styles are written only when they change.
|
|
43
|
+
setStyle(panel, "minWidth", `${Math.round(Math.min(anchor.width, window.innerWidth - 2 * margin))}px`);
|
|
44
|
+
setStyle(panel, "maxHeight", `${Math.max(0, Math.floor(Math.min(maxHeight, side === "below" ? roomBelow : roomAbove)))}px`);
|
|
45
|
+
const width = panel.offsetWidth;
|
|
46
|
+
const height = panel.offsetHeight;
|
|
47
|
+
const top = side === "below" ? anchor.bottom + gap : anchor.top - gap - height;
|
|
48
|
+
const left = Math.max(margin, Math.min(anchor.left, window.innerWidth - width - margin));
|
|
49
|
+
setStyle(panel, "top", `${Math.round(top)}px`);
|
|
50
|
+
setStyle(panel, "left", `${Math.round(left)}px`);
|
|
51
|
+
return side;
|
|
52
|
+
}
|
|
53
|
+
function setStyle(element, property, value) {
|
|
54
|
+
if (element.style[property] !== value)
|
|
55
|
+
element.style[property] = value;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Places a panel at a point, such as where the user right-clicked: to the right of and below the point when there
|
|
59
|
+
* is room, otherwise to the left of or above it, and never past the window's edges.
|
|
60
|
+
*/
|
|
61
|
+
export function placeAtPoint(panel, x, y, margin = 8) {
|
|
62
|
+
const width = panel.offsetWidth;
|
|
63
|
+
const height = panel.offsetHeight;
|
|
64
|
+
let left = x;
|
|
65
|
+
let top = y;
|
|
66
|
+
if (left + width > window.innerWidth - margin)
|
|
67
|
+
left = x - width;
|
|
68
|
+
if (top + height > window.innerHeight - margin)
|
|
69
|
+
top = y - height;
|
|
70
|
+
panel.style.left = `${Math.round(Math.max(margin, Math.min(left, window.innerWidth - width - margin)))}px`;
|
|
71
|
+
panel.style.top = `${Math.round(Math.max(margin, Math.min(top, window.innerHeight - height - margin)))}px`;
|
|
72
|
+
}
|
|
73
|
+
/** Runs `place` again, once per frame at most, while the page scrolls or the window changes size. Returns the stop function. */
|
|
74
|
+
export function followAnchor(place) {
|
|
75
|
+
let frame = 0;
|
|
76
|
+
const reposition = () => {
|
|
77
|
+
cancelAnimationFrame(frame);
|
|
78
|
+
frame = requestAnimationFrame(place);
|
|
79
|
+
};
|
|
80
|
+
window.addEventListener("scroll", reposition, { capture: true, passive: true });
|
|
81
|
+
window.addEventListener("resize", reposition);
|
|
82
|
+
return () => {
|
|
83
|
+
cancelAnimationFrame(frame);
|
|
84
|
+
window.removeEventListener("scroll", reposition, { capture: true });
|
|
85
|
+
window.removeEventListener("resize", reposition);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Whether the popover is shown. Safe on browsers without popover support, where it is never "open". */
|
|
89
|
+
export function isPopoverOpen(panel) {
|
|
90
|
+
return Boolean(panel && typeof panel.matches === "function" && panel.matches(":popover-open"));
|
|
91
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** How a template token is written in the text: `|Key|` (reports) or `{{Key}}` (messages). */
|
|
2
|
+
export type RichTextTokenFormat = "|key|" | "{{key}}";
|
|
3
|
+
/** A value a template fills in, such as the guest's name. */
|
|
4
|
+
export interface RichTextToken {
|
|
5
|
+
/** The key the template engine fills, e.g. "CustomerName": letters, digits, `_`, and `.`, starting with a letter or `_`. */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Shown on the chip in the editor and in its menu. Default: the key. */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** A second line in the menu, e.g. "The guest's full name". */
|
|
10
|
+
description?: string;
|
|
11
|
+
/** A heading the menu groups tokens under, e.g. "Guest". */
|
|
12
|
+
group?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface RichTextOptions {
|
|
15
|
+
/** Keeps images (`<img>` with an http, https, relative, or `data:image` address). Default false: images are removed. */
|
|
16
|
+
images?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface RichTextRenderOptions extends RichTextOptions {
|
|
19
|
+
/** Links to other sites open in a new tab. The stored HTML never has a target; it is added when shown. */
|
|
20
|
+
newTab?: boolean;
|
|
21
|
+
/** How tokens are written, for `values`. */
|
|
22
|
+
tokenFormat?: RichTextTokenFormat;
|
|
23
|
+
/** Fills tokens with these values, as text. Tokens without a value stay as they are. */
|
|
24
|
+
values?: Readonly<Record<string, string | number | null | undefined>>;
|
|
25
|
+
}
|
|
26
|
+
export interface RichTextEditorHtmlOptions extends RichTextOptions {
|
|
27
|
+
/** Tokens written this way show as chips. */
|
|
28
|
+
tokenFormat?: RichTextTokenFormat;
|
|
29
|
+
/** The known tokens: their labels go on the chips, and other keys are marked unknown. */
|
|
30
|
+
tokens?: readonly RichTextToken[];
|
|
31
|
+
}
|
|
32
|
+
/** Rules for rich text. Lengths count the characters a reader sees, not the HTML. */
|
|
33
|
+
export interface RichTextRules {
|
|
34
|
+
required?: boolean;
|
|
35
|
+
/** Fewest characters of text. Not checked on empty text; use `required` for that. */
|
|
36
|
+
minLength?: number;
|
|
37
|
+
/** Most characters of text. */
|
|
38
|
+
maxLength?: number;
|
|
39
|
+
/** Runs after the other rules pass, with the HTML and its plain text. */
|
|
40
|
+
validate?: (html: string, text: string) => string | null | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The HTML a rich text editor stores: only the tags, attributes, and styles it makes, rebuilt from scratch, so script,
|
|
44
|
+
* event handlers, `javascript:` links, and foreign styles cannot pass. Pastes from Word and Google Docs keep their
|
|
45
|
+
* bold, italic, underline, colors, lists, and tables. Empty text gives "".
|
|
46
|
+
*
|
|
47
|
+
* @example sanitizeRichText('<p onclick="x()">Hi <b>there</b><script>alert(1)</script></p>') // '<p>Hi <strong>there</strong></p>'
|
|
48
|
+
*/
|
|
49
|
+
export declare function sanitizeRichText(html: string | null | undefined, options?: RichTextOptions): string;
|
|
50
|
+
/** The same HTML as `sanitizeRichText`, one block per line and indented, for a source view. */
|
|
51
|
+
export declare function formatRichTextHtml(html: string | null | undefined, options?: RichTextOptions): string;
|
|
52
|
+
/**
|
|
53
|
+
* The text a reader sees: one line per paragraph, heading, or list item, line breaks kept, table cells separated by
|
|
54
|
+
* tabs, and spaces collapsed. Tokens stay as written.
|
|
55
|
+
*/
|
|
56
|
+
export declare function richTextToPlainText(html: string | null | undefined): string;
|
|
57
|
+
/** Whether rich text has nothing a reader sees: no text and no image. A rule or an empty table counts as empty. */
|
|
58
|
+
export declare function isRichTextEmpty(html: string | null | undefined): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* HTML for display (`RichTextView`): sanitized, with tokens filled from `values` and, with `newTab`, links to other
|
|
61
|
+
* sites opening in a new tab.
|
|
62
|
+
*/
|
|
63
|
+
export declare function renderRichTextHtml(html: string | null | undefined, options?: RichTextRenderOptions): string;
|
|
64
|
+
/** HTML for the editor itself: sanitized, with each token shown as a chip that is edited as one piece. */
|
|
65
|
+
export declare function richTextToEditorHtml(html: string | null | undefined, options?: RichTextEditorHtmlOptions): string;
|
|
66
|
+
/** The HTML of one token chip, as the editor inserts it. */
|
|
67
|
+
export declare function richTextTokenChip(key: string, format: RichTextTokenFormat, token?: RichTextToken | null, known?: boolean): string;
|
|
68
|
+
/** A token as the template engine reads it: `|Key|` or `{{Key}}`. */
|
|
69
|
+
export declare function formatRichTextToken(key: string, format: RichTextTokenFormat): string;
|
|
70
|
+
/** The keys of the tokens in the text, each once, in the order they first appear. */
|
|
71
|
+
export declare function findRichTextTokens(html: string | null | undefined, format: RichTextTokenFormat): string[];
|
|
72
|
+
/** Fills the tokens in sanitized HTML with values, as text. Tokens without a value stay as they are. */
|
|
73
|
+
export declare function fillRichTextTokens(html: string | null | undefined, values: Readonly<Record<string, string | number | null | undefined>>, format?: RichTextTokenFormat): string;
|
|
74
|
+
/** Checks rich text against its rules and returns the first message, or `null`. Lengths count the plain text. */
|
|
75
|
+
export declare function validateRichText(html: string | null | undefined, rules: RichTextRules, fieldName?: string): string | null;
|
|
76
|
+
/** A CSS color as `#rrggbb`: hex, `rgb()`, `rgba()`, or a common name. Transparent and unknown colors give `null`. */
|
|
77
|
+
export declare function parseColor(value: string): string | null;
|