nexus-shared 3.0.0 → 3.2.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 +442 -0
- package/README.md +1123 -1000
- package/dist/Client.Index.d.ts +6 -0
- package/dist/Client.Index.js +6 -0
- package/dist/Components/Chats/ChatParts.d.ts +2 -2
- package/dist/Components/Chats/ChatParts.js +3 -7
- package/dist/Components/Chats/ChatThread.js +3 -3
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/Menu.js +21 -9
- package/dist/Components/Documents/SplitButton.d.ts +3 -1
- package/dist/Components/Documents/SplitButton.js +7 -5
- package/dist/Components/Documents/TabButtons.d.ts +5 -2
- package/dist/Components/Documents/TabButtons.js +2 -2
- package/dist/Components/Forms/ApiForm.d.ts +1 -1
- package/dist/Components/Forms/ApiForm.js +1 -1
- package/dist/Components/Forms/Crud.js +486 -206
- package/dist/Components/Forms/CrudHistory.d.ts +66 -0
- package/dist/Components/Forms/CrudHistory.js +140 -0
- package/dist/Components/Forms/CrudParts.d.ts +5 -1
- package/dist/Components/Forms/CrudParts.js +38 -5
- package/dist/Components/Forms/ExcelImport.d.ts +23 -7
- package/dist/Components/Forms/ExcelImport.js +189 -60
- package/dist/Components/Forms/Form.js +9 -14
- package/dist/Components/Forms/SubmitForm.d.ts +11 -2
- package/dist/Components/Forms/SubmitForm.js +23 -12
- package/dist/Components/Inputs/CodeBox.d.ts +69 -0
- package/dist/Components/Inputs/CodeBox.js +316 -0
- package/dist/Components/Inputs/DatePicker.js +4 -1
- package/dist/Components/Inputs/DateRangePicker.js +4 -1
- package/dist/Components/Inputs/DateTimePicker.js +4 -1
- package/dist/Components/Inputs/Dropdown.js +4 -1
- package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
- package/dist/Components/Inputs/DropdownParts.js +4 -6
- package/dist/Components/Inputs/InputRenderer.js +3 -0
- package/dist/Components/Inputs/NumberBox.js +26 -2
- package/dist/Components/Inputs/RowsInput.js +2 -2
- package/dist/Components/Inputs/Slider.js +4 -1
- package/dist/Components/Inputs/TabularForm.js +42 -10
- package/dist/Components/Inputs/TextArea.js +4 -1
- package/dist/Components/Inputs/TextBox.js +4 -1
- package/dist/Components/Inputs/TimePanel.js +4 -3
- package/dist/Components/Inputs/TimePicker.js +4 -1
- package/dist/Components/Inputs/TreeView.d.ts +55 -5
- package/dist/Components/Inputs/TreeView.js +372 -23
- package/dist/Components/Layouts/Popup.js +1 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +21 -3
- package/dist/Components/Layouts/ThemePicker.js +89 -6
- package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
- package/dist/Components/Layouts/ThemeScript.js +4 -2
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
- package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
- package/dist/Components/Viewers/Avatar.d.ts +21 -0
- package/dist/Components/Viewers/Avatar.js +21 -0
- package/dist/Components/Viewers/DataTable.js +252 -62
- package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
- package/dist/Components/Viewers/DataTableColumns.js +25 -68
- package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
- package/dist/Components/Viewers/DataTableFilters.js +68 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +28 -10
- package/dist/Components/Viewers/DataTableParts.js +42 -13
- package/dist/Helpers/AnimationHelpers.d.ts +11 -0
- package/dist/Helpers/AnimationHelpers.js +20 -2
- package/dist/Helpers/ApiModules.d.ts +3 -34
- package/dist/Helpers/ApiModules.js +3 -3
- package/dist/Helpers/ApiRoutes.d.ts +68 -26
- package/dist/Helpers/ApiRoutes.js +31 -27
- package/dist/Helpers/ArrayHelpers.d.ts +0 -0
- package/dist/Helpers/ArrayHelpers.js +2 -0
- package/dist/Helpers/CodeHelpers.d.ts +97 -0
- package/dist/Helpers/CodeHelpers.js +332 -0
- package/dist/Helpers/CrudBackend.d.ts +101 -8
- package/dist/Helpers/CrudBackend.js +172 -11
- package/dist/Helpers/CrudColumns.d.ts +51 -0
- package/dist/Helpers/CrudColumns.js +69 -0
- package/dist/Helpers/CrudHelpers.d.ts +50 -5
- package/dist/Helpers/CrudHelpers.js +109 -12
- package/dist/Helpers/ExcelBackend.d.ts +3 -3
- package/dist/Helpers/ExcelBackend.js +8 -2
- package/dist/Helpers/ExcelHelpers.d.ts +36 -8
- package/dist/Helpers/ExcelHelpers.js +55 -13
- package/dist/Helpers/FormStore.d.ts +9 -5
- package/dist/Helpers/FormStore.js +25 -5
- package/dist/Helpers/InputParamsHelpers.js +8 -0
- package/dist/Helpers/MessageBuilder.js +3 -0
- package/dist/Helpers/NexusHelpers.d.ts +0 -0
- package/dist/Helpers/NexusHelpers.js +1 -0
- package/dist/Helpers/NumberHelpers.d.ts +7 -1
- package/dist/Helpers/NumberHelpers.js +41 -3
- package/dist/Helpers/PopoverHelpers.d.ts +17 -5
- package/dist/Helpers/PopoverHelpers.js +52 -17
- package/dist/Helpers/PopoverHooks.d.ts +24 -0
- package/dist/Helpers/PopoverHooks.js +62 -0
- package/dist/Helpers/RequestCache.d.ts +35 -0
- package/dist/Helpers/RequestCache.js +113 -0
- package/dist/Helpers/RowsStore.d.ts +7 -2
- package/dist/Helpers/RowsStore.js +15 -3
- package/dist/Helpers/TableFilters.d.ts +25 -0
- package/dist/Helpers/TableFilters.js +162 -0
- package/dist/Helpers/TableHelpers.d.ts +7 -4
- package/dist/Helpers/TableHelpers.js +15 -6
- package/dist/Helpers/TableJoins.d.ts +70 -0
- package/dist/Helpers/TableJoins.js +250 -0
- package/dist/Helpers/TableStore.d.ts +16 -3
- package/dist/Helpers/TableStore.js +25 -5
- package/dist/Helpers/ThemeHelpers.d.ts +21 -4
- package/dist/Helpers/ThemeHelpers.js +46 -9
- package/dist/Helpers/TreeStore.d.ts +225 -11
- package/dist/Helpers/TreeStore.js +683 -90
- package/dist/Helpers/UserPreferences.d.ts +22 -0
- package/dist/Helpers/UserPreferences.js +116 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +67 -10
- package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
- package/dist/Interfaces/FormInterfaces.d.ts +37 -6
- package/dist/Interfaces/InputInterfaces.d.ts +24 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +51 -0
- package/dist/Interfaces/PreferenceInterfaces.js +25 -0
- package/dist/Interfaces/TableInterfaces.d.ts +145 -16
- package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
- package/dist/Interfaces/ThemeInterfaces.js +15 -0
- package/dist/Services/ApiProxy.d.ts +36 -1
- package/dist/Services/ApiProxy.js +182 -52
- package/dist/Services/ApiProxyRules.d.ts +203 -0
- package/dist/Services/ApiProxyRules.js +426 -0
- package/dist/Services/ExcelApi.d.ts +3 -1
- package/dist/Services/ExcelApi.js +1 -1
- package/dist/Services/JoinSources.d.ts +48 -0
- package/dist/Services/JoinSources.js +74 -0
- package/dist/Services/ServerApi.d.ts +13 -2
- package/dist/Services/ServerApi.js +8 -3
- package/dist/Services/ThemeService.d.ts +18 -2
- package/dist/Services/ThemeService.js +80 -16
- package/dist/Shared.Index.d.ts +6 -0
- package/dist/Shared.Index.js +6 -0
- package/package.json +4 -2
- package/src/Styles/Nexus.Avatar.css +79 -0
- package/src/Styles/Nexus.Base.css +51 -4
- package/src/Styles/Nexus.Button.css +27 -0
- package/src/Styles/Nexus.Calendar.css +4 -6
- package/src/Styles/Nexus.Chat.css +5 -51
- package/src/Styles/Nexus.Choice.css +4 -6
- package/src/Styles/Nexus.Crud.css +16 -9
- package/src/Styles/Nexus.Dropdown.css +12 -61
- package/src/Styles/Nexus.Editor.css +4 -6
- package/src/Styles/Nexus.Excel.css +34 -15
- package/src/Styles/Nexus.Form.css +25 -67
- package/src/Styles/Nexus.Index.css +1 -0
- package/src/Styles/Nexus.Input.css +794 -637
- package/src/Styles/Nexus.Markdown.css +0 -28
- package/src/Styles/Nexus.Menu.css +8 -30
- package/src/Styles/Nexus.Popup.css +22 -29
- package/src/Styles/Nexus.RichText.css +2 -32
- package/src/Styles/Nexus.Rows.css +51 -10
- package/src/Styles/Nexus.Slider.css +3 -5
- package/src/Styles/Nexus.Tab.Buttons.css +20 -7
- package/src/Styles/Nexus.Table.css +1387 -1296
- package/src/Styles/Nexus.Theme.Picker.css +342 -232
- package/src/Styles/Nexus.Time.css +9 -12
- package/src/Styles/Nexus.Toast.css +6 -8
- package/src/Styles/Nexus.Tokens.css +17 -0
- package/src/Styles/Nexus.Tree.css +76 -11
|
@@ -53,8 +53,8 @@ export class NexusExcelBackend extends ExcelBackend {
|
|
|
53
53
|
get uploadField() {
|
|
54
54
|
return "file";
|
|
55
55
|
}
|
|
56
|
-
importRequest(rows) {
|
|
57
|
-
return { rows };
|
|
56
|
+
importRequest(rows, recordStatus) {
|
|
57
|
+
return recordStatus === undefined ? { rows } : { rows, recordStatus };
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* The same request a Nexus listing takes, minus `skipCount` and `maxCount`: an export writes every row the search
|
|
@@ -79,6 +79,10 @@ export class NexusExcelBackend extends ExcelBackend {
|
|
|
79
79
|
maxImportRows: count(answer.maxImportRows, DEFAULT_MAX_IMPORT_ROWS),
|
|
80
80
|
columns: readColumns(answer.columns),
|
|
81
81
|
inputs: Array.isArray(answer.inputs) ? answer.inputs : [],
|
|
82
|
+
recordStatuses: Array.isArray(answer.recordStatuses)
|
|
83
|
+
? answer.recordStatuses.filter(isRecord).flatMap(status => (typeof status.value === "number" && typeof status.label === "string" ? [{ value: status.value, label: status.label }] : []))
|
|
84
|
+
: [],
|
|
85
|
+
defaultRecordStatus: typeof answer.defaultRecordStatus === "number" ? answer.defaultRecordStatus : undefined,
|
|
82
86
|
};
|
|
83
87
|
}
|
|
84
88
|
readVerify(result) {
|
|
@@ -233,6 +237,8 @@ export function readExcelResponse(body, status) {
|
|
|
233
237
|
messageCode: text(first.messageCode) ?? undefined,
|
|
234
238
|
errorCode: first.errorCode === undefined ? undefined : count(first.errorCode),
|
|
235
239
|
...(Object.keys(errors).length ? { errors } : {}),
|
|
240
|
+
// An import that wrote nothing still says, row by row, what it refused and why (XLS_X044): keep that report.
|
|
241
|
+
...(isRecord(body.result) ? { result: body.result } : {}),
|
|
236
242
|
};
|
|
237
243
|
}
|
|
238
244
|
}
|
|
@@ -8,11 +8,14 @@ export declare const EXCEL_ROW_NUMBER = "__excelRow";
|
|
|
8
8
|
export declare const EXCEL_ROW_KEYS = "__excelKeys";
|
|
9
9
|
/** The field of an editor row that holds what the verify answered for it, so an edit can be told from an untouched row. */
|
|
10
10
|
export declare const EXCEL_ROW_SENT = "__excelSent";
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* One row of a review editor: a value per column, plus the three fields the editor carries for itself. A row added in
|
|
13
|
+
* the browser - by Add, Insert or Duplicate, or every row of a bulk insert - has none of the three, or only some.
|
|
14
|
+
*/
|
|
12
15
|
export interface ExcelEditorRow {
|
|
13
|
-
[EXCEL_ROW_NUMBER]
|
|
14
|
-
[EXCEL_ROW_KEYS]
|
|
15
|
-
[EXCEL_ROW_SENT]
|
|
16
|
+
[EXCEL_ROW_NUMBER]?: number;
|
|
17
|
+
[EXCEL_ROW_KEYS]?: Record<string, string | null>;
|
|
18
|
+
[EXCEL_ROW_SENT]?: Record<string, string | null>;
|
|
16
19
|
[field: string]: unknown;
|
|
17
20
|
}
|
|
18
21
|
declare const DEFAULT_TEXT: {
|
|
@@ -25,7 +28,15 @@ declare const DEFAULT_TEXT: {
|
|
|
25
28
|
readonly verifyFailed: "The file could not be read.";
|
|
26
29
|
readonly newRows: "New rows";
|
|
27
30
|
readonly duplicates: "Already there";
|
|
28
|
-
readonly duplicateHint: "
|
|
31
|
+
readonly duplicateHint: "Already records: these are skipped, and nothing here is updated.";
|
|
32
|
+
readonly failedHead: "Not written";
|
|
33
|
+
readonly skippedHead: "Skipped: already there";
|
|
34
|
+
readonly importAgain: "To import another file, start over.";
|
|
35
|
+
readonly addAgain: "To add more, start over.";
|
|
36
|
+
readonly bulkUnavailable: "The sheet's columns could not be read, so there is nothing to type the rows into.";
|
|
37
|
+
readonly discardTitle: "Discard what you typed?";
|
|
38
|
+
readonly discardLabel: "Discard";
|
|
39
|
+
readonly keepEditing: "Keep editing";
|
|
29
40
|
readonly toFix: "What needs fixing";
|
|
30
41
|
readonly whyDuplicate: "Why these are already there";
|
|
31
42
|
readonly referenceLocked: "Picked in the spreadsheet: this list belongs to the server.";
|
|
@@ -66,18 +77,35 @@ export declare function isExcelColumnEditable(column: ExcelColumnInfo): boolean;
|
|
|
66
77
|
* A reference cell shows the **words** the person picked, because its key is not something anybody reads or types.
|
|
67
78
|
*/
|
|
68
79
|
export declare function excelEditorRows(rows: readonly ExcelReviewRow[], columns: readonly ExcelColumnInfo[]): ExcelEditorRow[];
|
|
80
|
+
/** The sheet row a row came from, or 0 for a row added in the browser. */
|
|
81
|
+
export declare function excelRowNumber(row: ExcelEditorRow): number;
|
|
69
82
|
/**
|
|
70
83
|
* The editors' rows as rows to send: every value as text, every reference as the key it came with, and the sheet's own
|
|
71
84
|
* row number kept so a problem can still name where it is.
|
|
72
85
|
*
|
|
73
86
|
* A cell the person did not touch goes back **exactly** as it came, which is what makes the round trip faithful: the
|
|
74
87
|
* backend reads its own text again, not a number an input rewrote on the way through.
|
|
88
|
+
*
|
|
89
|
+
* A row added in the browser has no sheet row, so it is numbered after the last one the sheet had: no two rows share a
|
|
90
|
+
* number, and a problem still names the row it is about. `after` is that last row - the file's, rows already there and
|
|
91
|
+
* rows taken out of the review included, which are not among the rows sent. A bulk insert, with no sheet at all,
|
|
92
|
+
* numbers its rows 1, 2, 3.
|
|
75
93
|
*/
|
|
76
|
-
export declare function excelSheetRows(rows: readonly ExcelEditorRow[], columns: readonly ExcelColumnInfo[]): ExcelSheetRow[];
|
|
94
|
+
export declare function excelSheetRows(rows: readonly ExcelEditorRow[], columns: readonly ExcelColumnInfo[], after?: number): ExcelSheetRow[];
|
|
77
95
|
/** Whether the person changed anything in a row since the verify answered it. */
|
|
78
96
|
export declare function isExcelRowEdited(row: ExcelEditorRow, columns: readonly ExcelColumnInfo[]): boolean;
|
|
79
|
-
/**
|
|
80
|
-
export declare function
|
|
97
|
+
/** Whether a row has anything in a cell the person types in. A row added and left empty has not, and is not sent. */
|
|
98
|
+
export declare function hasExcelValues(row: ExcelEditorRow, columns: readonly ExcelColumnInfo[]): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Whether a row holds something only this screen has, which closing it would lose: a sheet's row the person edited, or
|
|
101
|
+
* a row added here with anything in it.
|
|
102
|
+
*/
|
|
103
|
+
export declare function isExcelRowTyped(row: ExcelEditorRow, columns: readonly ExcelColumnInfo[]): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Where a problem is, as the person reads it: the cell, a middle dot, and the column's heading ("B7 - City name"). Rows
|
|
106
|
+
* typed in the browser have no sheet, so `byRow` names the row instead ("Row 3 - City name").
|
|
107
|
+
*/
|
|
108
|
+
export declare function excelProblemPlace(problem: ExcelCellProblem, byRow?: boolean): string;
|
|
81
109
|
/** Every problem of these rows, in the order the rows read. */
|
|
82
110
|
export declare function excelProblems(rows: readonly ExcelReviewRow[]): ExcelCellProblem[];
|
|
83
111
|
/** The rows of one kind. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ExcelDataType, ExcelRowKind } from "../Interfaces/ExcelInterfaces.js";
|
|
2
|
+
import { toIsoDate } from "./DateHelpers.js";
|
|
3
|
+
import { toIsoTime } from "./TimeHelpers.js";
|
|
2
4
|
/*
|
|
3
5
|
* Turning a sheet's columns into inputs, a verify's rows into editable rows and back, and handing the person a file.
|
|
4
6
|
* Nothing here calls anything: the calls are in Services/ExcelApi.ts.
|
|
@@ -28,7 +30,15 @@ const DEFAULT_TEXT = {
|
|
|
28
30
|
verifyFailed: "The file could not be read.",
|
|
29
31
|
newRows: "New rows",
|
|
30
32
|
duplicates: "Already there",
|
|
31
|
-
duplicateHint: "
|
|
33
|
+
duplicateHint: "Already records: these are skipped, and nothing here is updated.",
|
|
34
|
+
failedHead: "Not written",
|
|
35
|
+
skippedHead: "Skipped: already there",
|
|
36
|
+
importAgain: "To import another file, start over.",
|
|
37
|
+
addAgain: "To add more, start over.",
|
|
38
|
+
bulkUnavailable: "The sheet's columns could not be read, so there is nothing to type the rows into.",
|
|
39
|
+
discardTitle: "Discard what you typed?",
|
|
40
|
+
discardLabel: "Discard",
|
|
41
|
+
keepEditing: "Keep editing",
|
|
32
42
|
toFix: "What needs fixing",
|
|
33
43
|
whyDuplicate: "Why these are already there",
|
|
34
44
|
referenceLocked: "Picked in the spreadsheet: this list belongs to the server.",
|
|
@@ -159,12 +169,15 @@ export function excelEditorRows(rows, columns) {
|
|
|
159
169
|
/** Which types are drawn by a number box, whose empty value is `null`. */
|
|
160
170
|
const NUMBER_TYPES = new Set([ExcelDataType.WholeNumber, ExcelDataType.Decimal, ExcelDataType.Amount]);
|
|
161
171
|
/**
|
|
162
|
-
* A cell's text as the input that draws it wants it: a number as a number, a choice unset rather than empty,
|
|
163
|
-
* everything else as it reads.
|
|
172
|
+
* A cell's text as the input that draws it wants it: a number as a number, a choice unset rather than empty, a date
|
|
173
|
+
* and a time as the inputs hold them, and everything else as it reads.
|
|
164
174
|
*
|
|
165
175
|
* The three empties are not interchangeable. A number box is empty at `null`; a **dropdown** given `""` treats it as a
|
|
166
176
|
* value and reports it as one its list no longer offers ("Not found"), so an empty choice has to be left unset; a text
|
|
167
177
|
* or date box is empty at `""`.
|
|
178
|
+
*
|
|
179
|
+
* A date and a time are read the way their inputs read them: the sheet's "09:30:00" is the time box's "09:30". Read
|
|
180
|
+
* any other way, a cell nobody touched would compare as an edit - and the panel would ask before closing over it.
|
|
168
181
|
*/
|
|
169
182
|
function toInputValue(column, value) {
|
|
170
183
|
if (value === null || value === "")
|
|
@@ -184,6 +197,10 @@ function toInputValue(column, value) {
|
|
|
184
197
|
return "No";
|
|
185
198
|
return value;
|
|
186
199
|
}
|
|
200
|
+
case ExcelDataType.Date:
|
|
201
|
+
return toIsoDate(value) ?? value;
|
|
202
|
+
case ExcelDataType.Time:
|
|
203
|
+
return toIsoTime(value) ?? value;
|
|
187
204
|
default:
|
|
188
205
|
return value;
|
|
189
206
|
}
|
|
@@ -210,17 +227,28 @@ function isSameCell(column, current, sent) {
|
|
|
210
227
|
return true;
|
|
211
228
|
return toInputValue(column, asSheet) === toInputValue(column, sent ?? null);
|
|
212
229
|
}
|
|
230
|
+
/** The sheet row a row came from, or 0 for a row added in the browser. */
|
|
231
|
+
export function excelRowNumber(row) {
|
|
232
|
+
const number = Number(row[EXCEL_ROW_NUMBER]);
|
|
233
|
+
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
234
|
+
}
|
|
213
235
|
/**
|
|
214
236
|
* The editors' rows as rows to send: every value as text, every reference as the key it came with, and the sheet's own
|
|
215
237
|
* row number kept so a problem can still name where it is.
|
|
216
238
|
*
|
|
217
239
|
* A cell the person did not touch goes back **exactly** as it came, which is what makes the round trip faithful: the
|
|
218
240
|
* backend reads its own text again, not a number an input rewrote on the way through.
|
|
241
|
+
*
|
|
242
|
+
* A row added in the browser has no sheet row, so it is numbered after the last one the sheet had: no two rows share a
|
|
243
|
+
* number, and a problem still names the row it is about. `after` is that last row - the file's, rows already there and
|
|
244
|
+
* rows taken out of the review included, which are not among the rows sent. A bulk insert, with no sheet at all,
|
|
245
|
+
* numbers its rows 1, 2, 3.
|
|
219
246
|
*/
|
|
220
|
-
export function excelSheetRows(rows, columns) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
247
|
+
export function excelSheetRows(rows, columns, after = 0) {
|
|
248
|
+
let next = rows.reduce((last, row) => Math.max(last, excelRowNumber(row)), Math.max(0, after));
|
|
249
|
+
return rows.map(row => {
|
|
250
|
+
const keys = row[EXCEL_ROW_KEYS] ?? {};
|
|
251
|
+
const sent = row[EXCEL_ROW_SENT] ?? {};
|
|
224
252
|
const values = {};
|
|
225
253
|
const labels = {};
|
|
226
254
|
for (const column of columns) {
|
|
@@ -231,13 +259,12 @@ export function excelSheetRows(rows, columns) {
|
|
|
231
259
|
}
|
|
232
260
|
values[column.field] = isSameCell(column, row[column.field], sent[column.field]) ? (sent[column.field] ?? null) : toSheetValue(row[column.field]);
|
|
233
261
|
}
|
|
234
|
-
|
|
235
|
-
return { number: Number.isFinite(number) && number > 0 ? number : index + 1, values, labels };
|
|
262
|
+
return { number: excelRowNumber(row) || ++next, values, labels };
|
|
236
263
|
});
|
|
237
264
|
}
|
|
238
265
|
/** Whether the person changed anything in a row since the verify answered it. */
|
|
239
266
|
export function isExcelRowEdited(row, columns) {
|
|
240
|
-
const sent =
|
|
267
|
+
const sent = row[EXCEL_ROW_SENT] ?? {};
|
|
241
268
|
for (const column of columns) {
|
|
242
269
|
if (!isExcelColumnEditable(column))
|
|
243
270
|
continue;
|
|
@@ -246,10 +273,25 @@ export function isExcelRowEdited(row, columns) {
|
|
|
246
273
|
}
|
|
247
274
|
return false;
|
|
248
275
|
}
|
|
276
|
+
/** Whether a row has anything in a cell the person types in. A row added and left empty has not, and is not sent. */
|
|
277
|
+
export function hasExcelValues(row, columns) {
|
|
278
|
+
return columns.some(column => isExcelColumnEditable(column) && toSheetValue(row[column.field]) !== null);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Whether a row holds something only this screen has, which closing it would lose: a sheet's row the person edited, or
|
|
282
|
+
* a row added here with anything in it.
|
|
283
|
+
*/
|
|
284
|
+
export function isExcelRowTyped(row, columns) {
|
|
285
|
+
return excelRowNumber(row) > 0 ? isExcelRowEdited(row, columns) : hasExcelValues(row, columns);
|
|
286
|
+
}
|
|
249
287
|
/* What is wrong, in the person's words */
|
|
250
|
-
/**
|
|
251
|
-
|
|
252
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Where a problem is, as the person reads it: the cell, a middle dot, and the column's heading ("B7 - City name"). Rows
|
|
290
|
+
* typed in the browser have no sheet, so `byRow` names the row instead ("Row 3 - City name").
|
|
291
|
+
*/
|
|
292
|
+
export function excelProblemPlace(problem, byRow = false) {
|
|
293
|
+
const place = byRow ? `Row ${problem.row}` : problem.cell;
|
|
294
|
+
return place ? `${place} ${String.fromCharCode(183)} ${problem.header}` : problem.header;
|
|
253
295
|
}
|
|
254
296
|
/** Every problem of these rows, in the order the rows read. */
|
|
255
297
|
export function excelProblems(rows) {
|
|
@@ -25,7 +25,7 @@ export interface FormStatus {
|
|
|
25
25
|
changed: boolean;
|
|
26
26
|
/** A submit is running. */
|
|
27
27
|
submitting: boolean;
|
|
28
|
-
/** The values are loading: the inputs show
|
|
28
|
+
/** The values are loading: the inputs show, disabled, and take nothing typed until the values are in. */
|
|
29
29
|
loading: boolean;
|
|
30
30
|
/** A submit ran and found problems: every message shows. */
|
|
31
31
|
showAll: boolean;
|
|
@@ -85,6 +85,8 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
85
85
|
private draftSeq;
|
|
86
86
|
/** The user cancelled: no draft is written until they change a value again. */
|
|
87
87
|
private draftDropped;
|
|
88
|
+
/** An input reported a value while the values loaded and was refused: it is drawn again from the store when loading ends. */
|
|
89
|
+
private refusedWhileLoading;
|
|
88
90
|
/** A load during a render tells nobody; `announce` does, once the form has mounted. */
|
|
89
91
|
private quiet;
|
|
90
92
|
private unannounced;
|
|
@@ -137,13 +139,15 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
137
139
|
* Loads values, such as a record from a server. They become the values the form compares with, and every input shows
|
|
138
140
|
* them. With `keepChanged`, the values the user changed stay (a refresh that merges): only the others change, and the
|
|
139
141
|
* names returned are the changed values whose loaded ones also changed, so the page can say so. `draft: false` leaves
|
|
140
|
-
* a saved draft for a later load (the form
|
|
141
|
-
* until `announce()`, for a load during a render.
|
|
142
|
+
* a saved draft for a later load (the form shows disabled while the real values load). `quiet` tells no listener
|
|
143
|
+
* until `announce()`, for a load during a render. `loading` opens the form loading, so whatever reads its status in
|
|
144
|
+
* the same render — a `FormButtons` after it — draws its loading state from the first frame, on the server too.
|
|
142
145
|
*/
|
|
143
|
-
load(values?: Partial<TValues>, { keepChanged, draft, quiet }?: {
|
|
146
|
+
load(values?: Partial<TValues>, { keepChanged, draft, quiet, loading }?: {
|
|
144
147
|
keepChanged?: boolean;
|
|
145
148
|
draft?: boolean;
|
|
146
149
|
quiet?: boolean;
|
|
150
|
+
loading?: boolean;
|
|
147
151
|
}): string[];
|
|
148
152
|
/** Tells listeners about a quiet load. */
|
|
149
153
|
announce(): void;
|
|
@@ -197,7 +201,7 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
197
201
|
commit(saved: TValues, extra?: Partial<TValues>): void;
|
|
198
202
|
/** @internal A save after which the form went back to its opening values (`resetOnSuccess`). */
|
|
199
203
|
markSaved(): void;
|
|
200
|
-
/** @internal */
|
|
204
|
+
/** @internal While the values load, nothing typed is taken (`input`), and a submit waits (`SubmitForm`). */
|
|
201
205
|
setLoading(loading: boolean): void;
|
|
202
206
|
/** @internal */
|
|
203
207
|
setFormId(formId: string | null): void;
|
|
@@ -70,6 +70,8 @@ export class FormStore {
|
|
|
70
70
|
draftSeq = 0;
|
|
71
71
|
/** The user cancelled: no draft is written until they change a value again. */
|
|
72
72
|
draftDropped = false;
|
|
73
|
+
/** An input reported a value while the values loaded and was refused: it is drawn again from the store when loading ends. */
|
|
74
|
+
refusedWhileLoading = false;
|
|
73
75
|
/** A load during a render tells nobody; `announce` does, once the form has mounted. */
|
|
74
76
|
quiet = false;
|
|
75
77
|
unannounced = false;
|
|
@@ -216,14 +218,18 @@ export class FormStore {
|
|
|
216
218
|
* Loads values, such as a record from a server. They become the values the form compares with, and every input shows
|
|
217
219
|
* them. With `keepChanged`, the values the user changed stay (a refresh that merges): only the others change, and the
|
|
218
220
|
* names returned are the changed values whose loaded ones also changed, so the page can say so. `draft: false` leaves
|
|
219
|
-
* a saved draft for a later load (the form
|
|
220
|
-
* until `announce()`, for a load during a render.
|
|
221
|
+
* a saved draft for a later load (the form shows disabled while the real values load). `quiet` tells no listener
|
|
222
|
+
* until `announce()`, for a load during a render. `loading` opens the form loading, so whatever reads its status in
|
|
223
|
+
* the same render — a `FormButtons` after it — draws its loading state from the first frame, on the server too.
|
|
221
224
|
*/
|
|
222
|
-
load(values = {}, { keepChanged = false, draft = true, quiet = false } = {}) {
|
|
225
|
+
load(values = {}, { keepChanged = false, draft = true, quiet = false, loading } = {}) {
|
|
223
226
|
this.quiet = quiet;
|
|
224
227
|
this.unannounced ||= quiet;
|
|
225
228
|
try {
|
|
226
|
-
|
|
229
|
+
const changed = this.loadValues(values, keepChanged, draft);
|
|
230
|
+
if (loading !== undefined)
|
|
231
|
+
this.patchStatus({ loading });
|
|
232
|
+
return changed;
|
|
227
233
|
}
|
|
228
234
|
finally {
|
|
229
235
|
this.quiet = false;
|
|
@@ -264,8 +270,10 @@ export class FormStore {
|
|
|
264
270
|
this.nextRevision(params.name);
|
|
265
271
|
}
|
|
266
272
|
else {
|
|
273
|
+
// Every input mounts again with its loaded value, one that was typed in while loading included.
|
|
267
274
|
this.generation++;
|
|
268
275
|
this.touched.clear();
|
|
276
|
+
this.refusedWhileLoading = false;
|
|
269
277
|
}
|
|
270
278
|
this.serverErrors.clear();
|
|
271
279
|
this.serverMessage = null;
|
|
@@ -321,6 +329,12 @@ export class FormStore {
|
|
|
321
329
|
input(name, value, stamp) {
|
|
322
330
|
if (stamp !== this.stamp(name))
|
|
323
331
|
return;
|
|
332
|
+
// Nothing is taken while the values load, not even from an input whose `disabled` was taken off by hand: the
|
|
333
|
+
// loaded values would land on top of it, and a change to a record that is not there yet means nothing.
|
|
334
|
+
if (this.status.loading) {
|
|
335
|
+
this.refusedWhileLoading = true;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
324
338
|
this.change([[name, value]], "input");
|
|
325
339
|
}
|
|
326
340
|
/** Marks an input as left by the user: the form's rule messages on it show from now on. */
|
|
@@ -481,8 +495,14 @@ export class FormStore {
|
|
|
481
495
|
markSaved() {
|
|
482
496
|
this.patchStatus({ submitting: false, savedAt: Date.now() });
|
|
483
497
|
}
|
|
484
|
-
/** @internal */
|
|
498
|
+
/** @internal While the values load, nothing typed is taken (`input`), and a submit waits (`SubmitForm`). */
|
|
485
499
|
setLoading(loading) {
|
|
500
|
+
// Something was typed and refused, and no load has drawn the inputs again since: they show the store's values.
|
|
501
|
+
if (!loading && this.refusedWhileLoading) {
|
|
502
|
+
this.refusedWhileLoading = false;
|
|
503
|
+
this.generation++;
|
|
504
|
+
this.bumpAll();
|
|
505
|
+
}
|
|
486
506
|
this.patchStatus({ loading });
|
|
487
507
|
}
|
|
488
508
|
/** @internal */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_DATE_PREFERENCES } from "../Interfaces/DateInterfaces.js";
|
|
2
|
+
import { formatCode, loadCode, validateCode } from "./CodeHelpers.js";
|
|
2
3
|
import { formatDate, formatDateRange, toDateRange, toIsoDate, usesDevanagari } from "./DateHelpers.js";
|
|
3
4
|
import { formatDateTime, toUtcDateTime } from "./DateTimeHelpers.js";
|
|
4
5
|
import { formatNumber, parseNumber } from "./NumberHelpers.js";
|
|
@@ -108,6 +109,9 @@ export function normalizeInputValue(params, value) {
|
|
|
108
109
|
return Array.isArray(value) && value.length === 2 ? value : emptyInputValue(params);
|
|
109
110
|
case "radio-group":
|
|
110
111
|
return value;
|
|
112
|
+
case "code":
|
|
113
|
+
// Only separators and case: a saved code that does not fit stays as it is, so its check refuses the save.
|
|
114
|
+
return loadCode(value, params);
|
|
111
115
|
default:
|
|
112
116
|
return typeof value === "string" ? value : String(value);
|
|
113
117
|
}
|
|
@@ -197,6 +201,8 @@ export function validateInput(params, value, context = {}) {
|
|
|
197
201
|
seconds,
|
|
198
202
|
});
|
|
199
203
|
}
|
|
204
|
+
case "code":
|
|
205
|
+
return validateCode(value, params, name);
|
|
200
206
|
case "slider":
|
|
201
207
|
return params.validate?.(value) || null;
|
|
202
208
|
case "range-slider":
|
|
@@ -369,6 +375,8 @@ export function formatInputValue(params, value, context = {}) {
|
|
|
369
375
|
return formatTime(value, timeSettings(params, preferences));
|
|
370
376
|
case "date-time":
|
|
371
377
|
return formatDateTime(value, { ...dateSettings(params, preferences), timeFormat: params.timeFormat ?? preferences.timeFormat, timeZone: params.timeZone });
|
|
378
|
+
case "code":
|
|
379
|
+
return formatCode(value, params);
|
|
372
380
|
case "slider":
|
|
373
381
|
return formatNumber(value, params);
|
|
374
382
|
case "range-slider":
|
|
@@ -17,6 +17,8 @@ const DEFAULT_WORDS = {
|
|
|
17
17
|
unflag: { verb: "unflag", done: "unflagged", doing: "Unflagging", object: "the record" },
|
|
18
18
|
pin: { verb: "pin", done: "pinned", doing: "Pinning", object: "the record" },
|
|
19
19
|
unpin: { verb: "unpin", done: "unpinned", doing: "Unpinning", object: "the record" },
|
|
20
|
+
favorite: { verb: "add {object} to the favourites", done: "added to the favourites", doing: "Adding {object} to the favourites", object: "the record" },
|
|
21
|
+
unfavorite: { verb: "remove {object} from the favourites", done: "removed from the favourites", doing: "Removing {object} from the favourites", object: "the record" },
|
|
20
22
|
send: { verb: "send", done: "sent", doing: "Sending", object: "the message", sendsValues: true },
|
|
21
23
|
submit: { verb: "submit", done: "submitted", doing: "Submitting", object: "the form", sendsValues: true },
|
|
22
24
|
upload: { verb: "upload", done: "uploaded", doing: "Uploading", object: "the file", sendsValues: true },
|
|
@@ -43,6 +45,7 @@ const DEFAULT_TEXTS = {
|
|
|
43
45
|
confirmDetails: {
|
|
44
46
|
delete: "This cannot be undone.",
|
|
45
47
|
trash: "It can be restored from the trash.",
|
|
48
|
+
recover: "It goes back to the active list.",
|
|
46
49
|
},
|
|
47
50
|
reasons: {
|
|
48
51
|
network: "The server cannot be reached. Check your internet connection and try again.",
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NumberFormatOptions } from "../Interfaces/InputInterfaces.ts";
|
|
2
|
+
import { ENumberScales, type ThousandsSeparator } from "../Interfaces/PreferenceInterfaces.ts";
|
|
2
3
|
/** Default locale for number text. Fixed rather than the browser's, so server-rendered text matches the browser's. */
|
|
3
4
|
export declare const DEFAULT_NUMBER_LOCALE = "en-US";
|
|
4
5
|
/** Most digits a number field takes. A JavaScript number holds 15 significant digits exactly; beyond that, digits change. */
|
|
@@ -15,7 +16,12 @@ export interface NumberTextRules {
|
|
|
15
16
|
/** Allows a leading minus sign. */
|
|
16
17
|
negative: boolean;
|
|
17
18
|
}
|
|
18
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The locale for a number scale and a thousands separator: `en-US` (1,234,567.5) and `en-IN` (12,34,567.5) as they are,
|
|
21
|
+
* else a key such as "en-US|.|," (1.234.567,5) that `formatNumber`, `parseNumber` and `getNumberSymbols` read.
|
|
22
|
+
*/
|
|
23
|
+
export declare function numberLocale(scale: ENumberScales, thousands: ThousandsSeparator): string;
|
|
24
|
+
/** The decimal and group separators of a locale (or of a key from `numberLocale`). */
|
|
19
25
|
export declare function getNumberSymbols(locale?: string): NumberSymbols;
|
|
20
26
|
/**
|
|
21
27
|
* Formats a number for display: `formatNumber(1250, { decimals: 2, fixedDecimals: true, grouping: true })` gives "1,250.00".
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ENumberScales } from "../Interfaces/PreferenceInterfaces.js";
|
|
1
2
|
/** Default locale for number text. Fixed rather than the browser's, so server-rendered text matches the browser's. */
|
|
2
3
|
export const DEFAULT_NUMBER_LOCALE = "en-US";
|
|
3
4
|
/** Most digits a number field takes. A JavaScript number holds 15 significant digits exactly; beyond that, digits change. */
|
|
@@ -9,9 +10,43 @@ const BIDI_MARKS = /[\u061c\u200e\u200f]/g;
|
|
|
9
10
|
// U+2212, the typographic minus sign some locales use.
|
|
10
11
|
const MINUS_SIGN = "\u2212";
|
|
11
12
|
const SPACES = /\s/g;
|
|
12
|
-
|
|
13
|
+
/*
|
|
14
|
+
* A person's number settings as a locale: the scale picks where the groups fall (en-US, or en-IN for lakh and crore)
|
|
15
|
+
* and the separator what marks them. A pair Intl already has is that locale; any other is a key of three parts,
|
|
16
|
+
* "en-IN|.|,", that the helpers here read as the base locale's grouping with these two marks. Everything that takes
|
|
17
|
+
* a `locale` (a number box, a table cell, a message) therefore takes the person's settings without knowing of them.
|
|
18
|
+
*/
|
|
19
|
+
const SEPARATOR_MARKS = {
|
|
20
|
+
period: { group: ".", decimal: "," },
|
|
21
|
+
space: { group: "\u00a0", decimal: "." },
|
|
22
|
+
none: { group: "", decimal: "." },
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The locale for a number scale and a thousands separator: `en-US` (1,234,567.5) and `en-IN` (12,34,567.5) as they are,
|
|
26
|
+
* else a key such as "en-US|.|," (1.234.567,5) that `formatNumber`, `parseNumber` and `getNumberSymbols` read.
|
|
27
|
+
*/
|
|
28
|
+
export function numberLocale(scale, thousands) {
|
|
29
|
+
const base = scale === ENumberScales.SouthAsian ? "en-IN" : DEFAULT_NUMBER_LOCALE;
|
|
30
|
+
if (thousands === "comma" || !(thousands in SEPARATOR_MARKS))
|
|
31
|
+
return base;
|
|
32
|
+
const { group, decimal } = SEPARATOR_MARKS[thousands];
|
|
33
|
+
return `${base}|${group}|${decimal}`;
|
|
34
|
+
}
|
|
35
|
+
/** A locale key from `numberLocale`: its base locale and its own marks, or `null` for a plain locale. */
|
|
36
|
+
function customMarks(locale) {
|
|
37
|
+
if (!locale.includes("|"))
|
|
38
|
+
return null;
|
|
39
|
+
const [base = DEFAULT_NUMBER_LOCALE, group = ",", decimal = "."] = locale.split("|");
|
|
40
|
+
return { base, group, decimal };
|
|
41
|
+
}
|
|
42
|
+
/** The decimal and group separators of a locale (or of a key from `numberLocale`). */
|
|
13
43
|
export function getNumberSymbols(locale = DEFAULT_NUMBER_LOCALE) {
|
|
14
44
|
let symbols = symbolsByLocale.get(locale);
|
|
45
|
+
const custom = symbols ? null : customMarks(locale);
|
|
46
|
+
if (custom) {
|
|
47
|
+
symbols = { decimal: custom.decimal, group: custom.group };
|
|
48
|
+
symbolsByLocale.set(locale, symbols);
|
|
49
|
+
}
|
|
15
50
|
if (!symbols) {
|
|
16
51
|
const parts = new Intl.NumberFormat(locale).formatToParts(1234567.5);
|
|
17
52
|
symbols = {
|
|
@@ -33,9 +68,10 @@ export function formatNumber(value, options = {}) {
|
|
|
33
68
|
const maxDecimals = decimals ?? MAX_NUMBER_DIGITS;
|
|
34
69
|
const minDecimals = fixedDecimals ? maxDecimals : 0;
|
|
35
70
|
const key = `${locale}|${minDecimals}|${maxDecimals}|${grouping}`;
|
|
71
|
+
const custom = customMarks(locale);
|
|
36
72
|
let formatter = formatters.get(key);
|
|
37
73
|
if (!formatter) {
|
|
38
|
-
formatter = new Intl.NumberFormat(locale, {
|
|
74
|
+
formatter = new Intl.NumberFormat(custom?.base ?? locale, {
|
|
39
75
|
numberingSystem: "latn",
|
|
40
76
|
useGrouping: grouping,
|
|
41
77
|
minimumFractionDigits: minDecimals,
|
|
@@ -44,7 +80,9 @@ export function formatNumber(value, options = {}) {
|
|
|
44
80
|
formatters.set(key, formatter);
|
|
45
81
|
}
|
|
46
82
|
// Rounded first, the same way as roundDecimal, which also turns -0.001 into 0 rather than "-0".
|
|
47
|
-
|
|
83
|
+
const text = formatter.format(roundDecimal(value, maxDecimals)).replace(BIDI_MARKS, "").replace(MINUS_SIGN, "-");
|
|
84
|
+
// The base locales write "," between groups and "." before decimals; a key's own marks take their places.
|
|
85
|
+
return custom ? text.replace(/[,.]/g, mark => (mark === "," ? custom.group : custom.decimal)) : text;
|
|
48
86
|
}
|
|
49
87
|
/**
|
|
50
88
|
* Reads a number written in the locale's format: "1,250.50" gives 1250.5, and in `de-DE` "1.250,50" does.
|
|
@@ -20,8 +20,9 @@ export interface PlaceUnderAnchorOptions {
|
|
|
20
20
|
arrowAt?: DOMRect;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Places a panel under an anchor, or above it when
|
|
24
|
-
* edge and kept inside the window, with an arrow aimed at the anchor.
|
|
23
|
+
* Places a panel under an anchor, or above it when it fits only there, or on the side with more room when it fits on
|
|
24
|
+
* neither; aligned to the anchor's start or end edge and kept inside the window, with an arrow aimed at the anchor. A
|
|
25
|
+
* panel that scrolls (`.nx-popover__scroll`) is never taller than the room on its side. Returns whether it went above.
|
|
25
26
|
*/
|
|
26
27
|
export declare function placeUnderAnchor(panel: HTMLElement, anchor: DOMRect, options?: PlaceUnderAnchorOptions): boolean;
|
|
27
28
|
export interface PlaceListPanelOptions {
|
|
@@ -57,11 +58,22 @@ export interface PlaceAtPointOptions {
|
|
|
57
58
|
/**
|
|
58
59
|
* Places a panel at a point, such as where the user right-clicked: to the right of and below the point when there
|
|
59
60
|
* is room, otherwise to the left of or above it, and never past the window's edges. With an arrow, the panel is set
|
|
60
|
-
* back far enough along both axes for the arrow's tip to land on the point.
|
|
61
|
+
* back far enough along both axes for the arrow's tip to land on the point. A panel that scrolls is capped to the
|
|
62
|
+
* room on its side, as under an anchor.
|
|
61
63
|
*/
|
|
62
64
|
export declare function placeAtPoint(panel: HTMLElement, x: number, y: number, options?: PlaceAtPointOptions): void;
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Runs `place` again, once per frame at most, while the page scrolls, the window changes size, or `panel` does (its
|
|
67
|
+
* content grew). Returns the stop function. A frame later, never inside the observer, so a panel that `place` resizes
|
|
68
|
+
* is measured once more and settles instead of looping.
|
|
69
|
+
*/
|
|
70
|
+
export declare function followAnchor(place: () => void, panel?: Element): () => void;
|
|
71
|
+
/**
|
|
72
|
+
* Shows a panel with its scroller (`.nx-popover__scroll`) at the top. The browser keeps where a panel was scrolled
|
|
73
|
+
* while it was shut, and a list that opens part-way down has lost its heading and its first rows. Does nothing on
|
|
74
|
+
* browsers without popover support.
|
|
75
|
+
*/
|
|
76
|
+
export declare function showPanel(panel: HTMLElement): void;
|
|
65
77
|
/** Whether the popover is shown. Safe on browsers without popover support, where it is never "open". */
|
|
66
78
|
export declare function isPopoverOpen(panel: HTMLElement | null): boolean;
|
|
67
79
|
/** How far the arrow reaches past the panel's edge: its depth, less the panel's border it is drawn over. */
|