cx 26.7.6 → 26.9.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/build/charts/Legend.d.ts +8 -1
- package/build/charts/Legend.d.ts.map +1 -1
- package/build/charts/Legend.js +8 -4
- package/build/charts/LineGraph.d.ts +8 -16
- package/build/charts/LineGraph.d.ts.map +1 -1
- package/build/charts/LineGraph.js +76 -69
- package/build/charts/axis/NumericAxis.d.ts.map +1 -1
- package/build/charts/axis/NumericAxis.js +1 -254
- package/build/charts/axis/NumericScale.d.ts +59 -0
- package/build/charts/axis/NumericScale.d.ts.map +1 -0
- package/build/charts/axis/NumericScale.js +254 -0
- package/build/jsx-dev-runtime.d.ts +2 -1
- package/build/jsx-dev-runtime.d.ts.map +1 -1
- package/build/svg/Svg.d.ts +9 -0
- package/build/svg/Svg.d.ts.map +1 -1
- package/build/svg/Svg.js +9 -1
- package/build/ui/index.d.ts +3 -2
- package/build/ui/index.d.ts.map +1 -1
- package/build/ui/index.js +1 -1
- package/build/util/DOM.d.ts.map +1 -1
- package/build/util/DOM.js +16 -4
- package/build/util/getActiveElement.d.ts +1 -1
- package/build/util/getActiveElement.d.ts.map +1 -1
- package/build/util/getActiveElement.js +14 -2
- package/build/widgets/form/TextArea.d.ts +4 -0
- package/build/widgets/form/TextArea.d.ts.map +1 -1
- package/build/widgets/form/TextArea.js +7 -1
- package/build/widgets/grid/Grid.d.ts +12 -2
- package/build/widgets/grid/Grid.d.ts.map +1 -1
- package/build/widgets/grid/Grid.js +17 -4
- package/build/widgets/nav/MenuItem.js +2 -2
- package/build/widgets/overlay/Dropdown.d.ts +4 -4
- package/build/widgets/overlay/Dropdown.d.ts.map +1 -1
- package/build/widgets/overlay/Dropdown.js +82 -38
- package/build/widgets/overlay/Overlay.d.ts.map +1 -1
- package/build/widgets/overlay/Overlay.js +7 -3
- package/build/widgets/overlay/Window.d.ts.map +1 -1
- package/build/widgets/overlay/Window.js +2 -1
- package/dist/charts.js +166 -168
- package/dist/manifest.js +805 -802
- package/dist/svg.js +13 -0
- package/dist/util.js +31 -9
- package/dist/widgets.js +153 -50
- package/package.json +1 -1
- package/src/charts/Legend.tsx +22 -2
- package/src/charts/LineGraph.tsx +80 -98
- package/src/charts/axis/NumericAxis.spec.ts +40 -0
- package/src/charts/axis/NumericAxis.tsx +1 -290
- package/src/charts/axis/NumericScale.ts +290 -0
- package/src/jsx-dev-runtime.ts +2 -1
- package/src/svg/Svg.tsx +24 -1
- package/src/ui/Cx.tsx +505 -505
- package/src/ui/DataProxy.ts +55 -55
- package/src/ui/Rescope.ts +50 -50
- package/src/ui/exprHelpers.ts +96 -96
- package/src/ui/index.ts +3 -6
- package/src/util/DOM.ts +15 -4
- package/src/util/getActiveElement.ts +16 -2
- package/src/widgets/Sandbox.ts +104 -104
- package/src/widgets/form/ColorField.scss +112 -112
- package/src/widgets/form/DateTimeField.scss +111 -111
- package/src/widgets/form/LookupField.maps.scss +26 -26
- package/src/widgets/form/MonthField.scss +113 -113
- package/src/widgets/form/Select.scss +104 -104
- package/src/widgets/form/TextArea.tsx +10 -1
- package/src/widgets/form/TextField.scss +66 -66
- package/src/widgets/grid/Grid.sorting.spec.tsx +188 -0
- package/src/widgets/grid/Grid.tsx +31 -7
- package/src/widgets/index.ts +41 -41
- package/src/widgets/nav/MenuItem.tsx +2 -2
- package/src/widgets/overlay/Dropdown.tsx +87 -27
- package/src/widgets/overlay/Overlay.tsx +5 -3
- package/src/widgets/overlay/Window.tsx +2 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { Store } from "../../data/Store";
|
|
3
|
+
import { Grid } from "./Grid";
|
|
4
|
+
import { bind } from "../../ui/bind";
|
|
5
|
+
import { computable } from "../../data/computable";
|
|
6
|
+
import { createTestRenderer, act } from "../../util/test/createTestRenderer";
|
|
7
|
+
|
|
8
|
+
// priorities 1, 2, 10 sort differently as numbers (1, 2, 10) than their
|
|
9
|
+
// "P{priority}" labels do as strings (P1, P10, P2), which makes the sort key
|
|
10
|
+
// precedence (sortValue > sortField > value > field) observable in the output
|
|
11
|
+
let records = [
|
|
12
|
+
{ id: 1, priority: 2 },
|
|
13
|
+
{ id: 2, priority: 10 },
|
|
14
|
+
{ id: 3, priority: 1 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function textOf(node: any): string {
|
|
18
|
+
if (node == null) return "";
|
|
19
|
+
if (typeof node == "string") return node;
|
|
20
|
+
if (Array.isArray(node)) return node.map(textOf).join("");
|
|
21
|
+
return textOf(node.children);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findAll(node: any, predicate: (el: any) => boolean, result: any[] = []): any[] {
|
|
25
|
+
if (node == null || typeof node == "string") return result;
|
|
26
|
+
if (Array.isArray(node)) {
|
|
27
|
+
node.forEach((child) => findAll(child, predicate, result));
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
if (predicate(node)) result.push(node);
|
|
31
|
+
findAll(node.children, predicate, result);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getRenderedLabels(component: any): string[] {
|
|
36
|
+
let tds = findAll(component.toJSON(), (el) => el.type == "td");
|
|
37
|
+
return tds.map(textOf);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function clickHeader(component: any, text: string) {
|
|
41
|
+
let th = component.root.findAllByType("th").find((t: any) => thText(t) == text);
|
|
42
|
+
assert.ok(th, `Header cell "${text}" not found`);
|
|
43
|
+
await act(async () => {
|
|
44
|
+
th.props.onClick({ preventDefault() {}, stopPropagation() {} });
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function thText(th: any): string {
|
|
49
|
+
return th.children.map((c: any) => (typeof c == "string" ? c : "")).join("");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("Grid sorting precedence", () => {
|
|
53
|
+
it("sorts by sortField instead of the displayed value when both are set", async () => {
|
|
54
|
+
let widget = (
|
|
55
|
+
<cx>
|
|
56
|
+
<Grid
|
|
57
|
+
records={bind("records")}
|
|
58
|
+
columns={[
|
|
59
|
+
{
|
|
60
|
+
header: "Priority",
|
|
61
|
+
sortField: "priority",
|
|
62
|
+
value: { tpl: "P{$record.priority}" },
|
|
63
|
+
sortable: true,
|
|
64
|
+
},
|
|
65
|
+
]}
|
|
66
|
+
/>
|
|
67
|
+
</cx>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
let store = new Store({ data: { records } });
|
|
71
|
+
const component = await createTestRenderer(store, widget);
|
|
72
|
+
|
|
73
|
+
await clickHeader(component, "Priority");
|
|
74
|
+
assert.deepStrictEqual(getRenderedLabels(component), ["P1", "P2", "P10"]);
|
|
75
|
+
|
|
76
|
+
await clickHeader(component, "Priority");
|
|
77
|
+
assert.deepStrictEqual(getRenderedLabels(component), ["P10", "P2", "P1"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("sortValue takes precedence over sortField", async () => {
|
|
81
|
+
let widget = (
|
|
82
|
+
<cx>
|
|
83
|
+
<Grid
|
|
84
|
+
records={bind("records")}
|
|
85
|
+
columns={[
|
|
86
|
+
{
|
|
87
|
+
header: "Priority",
|
|
88
|
+
sortField: "priority",
|
|
89
|
+
sortValue: computable("$record.priority", (p: number) => -p),
|
|
90
|
+
value: { tpl: "P{$record.priority}" },
|
|
91
|
+
sortable: true,
|
|
92
|
+
},
|
|
93
|
+
]}
|
|
94
|
+
/>
|
|
95
|
+
</cx>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
let store = new Store({ data: { records } });
|
|
99
|
+
const component = await createTestRenderer(store, widget);
|
|
100
|
+
|
|
101
|
+
await clickHeader(component, "Priority");
|
|
102
|
+
assert.deepStrictEqual(getRenderedLabels(component), ["P10", "P2", "P1"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("sorts by the displayed value when no sortField is set", async () => {
|
|
106
|
+
let widget = (
|
|
107
|
+
<cx>
|
|
108
|
+
<Grid
|
|
109
|
+
records={bind("records")}
|
|
110
|
+
columns={[
|
|
111
|
+
{
|
|
112
|
+
header: "Priority",
|
|
113
|
+
field: "priority",
|
|
114
|
+
value: { tpl: "P{$record.priority}" },
|
|
115
|
+
sortable: true,
|
|
116
|
+
},
|
|
117
|
+
]}
|
|
118
|
+
/>
|
|
119
|
+
</cx>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
let store = new Store({ data: { records } });
|
|
123
|
+
const component = await createTestRenderer(store, widget);
|
|
124
|
+
|
|
125
|
+
await clickHeader(component, "Priority");
|
|
126
|
+
assert.deepStrictEqual(getRenderedLabels(component), ["P1", "P10", "P2"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("applies sortField precedence to sorters restored from sortField/sortDirection bindings", async () => {
|
|
130
|
+
let widget = (
|
|
131
|
+
<cx>
|
|
132
|
+
<Grid
|
|
133
|
+
records={bind("records")}
|
|
134
|
+
sortField={bind("sortField")}
|
|
135
|
+
sortDirection={bind("sortDirection")}
|
|
136
|
+
columns={[
|
|
137
|
+
{
|
|
138
|
+
header: "Priority",
|
|
139
|
+
sortField: "priority",
|
|
140
|
+
value: { tpl: "P{$record.priority}" },
|
|
141
|
+
sortable: true,
|
|
142
|
+
},
|
|
143
|
+
]}
|
|
144
|
+
/>
|
|
145
|
+
</cx>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
let store = new Store({
|
|
149
|
+
data: { records, sortField: "priority", sortDirection: "ASC" },
|
|
150
|
+
});
|
|
151
|
+
const component = await createTestRenderer(store, widget);
|
|
152
|
+
|
|
153
|
+
assert.deepStrictEqual(getRenderedLabels(component), ["P1", "P2", "P10"]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("marks only the clicked column as sorted", async () => {
|
|
157
|
+
let widget = (
|
|
158
|
+
<cx>
|
|
159
|
+
<Grid
|
|
160
|
+
records={bind("records")}
|
|
161
|
+
columns={[
|
|
162
|
+
{
|
|
163
|
+
header: "Priority",
|
|
164
|
+
sortField: "priority",
|
|
165
|
+
value: { tpl: "P{$record.priority}" },
|
|
166
|
+
sortable: true,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
header: "Label",
|
|
170
|
+
value: { tpl: "P{$record.priority}" },
|
|
171
|
+
sortable: true,
|
|
172
|
+
},
|
|
173
|
+
]}
|
|
174
|
+
/>
|
|
175
|
+
</cx>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
let store = new Store({ data: { records } });
|
|
179
|
+
const component = await createTestRenderer(store, widget);
|
|
180
|
+
|
|
181
|
+
await clickHeader(component, "Priority");
|
|
182
|
+
|
|
183
|
+
let ths = component.root.findAllByType("th");
|
|
184
|
+
let sorted = ths.filter((th: any) => (th.props.className || "").includes("sorted-asc"));
|
|
185
|
+
assert.strictEqual(sorted.length, 1);
|
|
186
|
+
assert.strictEqual(thText(sorted[0]), "Priority");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -72,10 +72,11 @@ import DropDownIcon from "../icons/sort-asc";
|
|
|
72
72
|
import { captureMouse2, getCursorPos } from "../overlay/captureMouse";
|
|
73
73
|
import { tooltipMouseLeave, tooltipMouseMove } from "../overlay/tooltip-ops";
|
|
74
74
|
import { createGridCellEditor } from "./createGridCellEditor";
|
|
75
|
-
import { GridRow, GridRowComponent,
|
|
75
|
+
import { GridRow, GridRowComponent, GridRowInstance } from "./GridRow";
|
|
76
|
+
import type { GridRowConfig } from "./GridRow";
|
|
76
77
|
import { Create, CreateConfig } from "../../util";
|
|
77
78
|
import type { TreeAdapter } from "../../ui/adapter/TreeAdapter";
|
|
78
|
-
export { GridRowConfig };
|
|
79
|
+
export type { GridRowConfig };
|
|
79
80
|
|
|
80
81
|
export type GridFetchRecordsResult<T> = T[] | { records: T[]; lastPage?: boolean; totalRecordCount?: number };
|
|
81
82
|
|
|
@@ -241,7 +242,16 @@ export interface GridColumnConfig {
|
|
|
241
242
|
children?: ChildNode | ChildNode[];
|
|
242
243
|
key?: string;
|
|
243
244
|
pad?: boolean;
|
|
245
|
+
/**
|
|
246
|
+
* Record field used for sorting instead of `field` or the displayed `value`.
|
|
247
|
+
* Use it when the column displays a computed value but should sort by raw data.
|
|
248
|
+
* Sort key precedence: `sortValue` > `sortField` > `value` > `field`.
|
|
249
|
+
*/
|
|
244
250
|
sortField?: string;
|
|
251
|
+
/**
|
|
252
|
+
* Selector (binding, template, expression or computable) used for sorting instead
|
|
253
|
+
* of `field`, `sortField` or the displayed `value`. Takes the highest precedence.
|
|
254
|
+
*/
|
|
245
255
|
sortValue?: Prop<any>;
|
|
246
256
|
style?: StyleProp;
|
|
247
257
|
trimWhitespace?: boolean;
|
|
@@ -944,9 +954,15 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
944
954
|
if (sortField) {
|
|
945
955
|
for (let l = 0; l < 10; l++) {
|
|
946
956
|
let line = instance.row[`line${l}`];
|
|
947
|
-
let sortColumn =
|
|
957
|
+
let sortColumn =
|
|
958
|
+
line && line.columns && line.columns.find((c: any) => (c.sortField || c.field) == sortField);
|
|
948
959
|
if (sortColumn) {
|
|
949
|
-
|
|
960
|
+
// precedence: sortValue > sortField > value > field
|
|
961
|
+
data.sorters[0].value = isDefined(sortColumn.sortValue)
|
|
962
|
+
? sortColumn.sortValue
|
|
963
|
+
: sortColumn.sortField
|
|
964
|
+
? undefined
|
|
965
|
+
: sortColumn.value;
|
|
950
966
|
data.sorters[0].comparer = sortColumn.comparer;
|
|
951
967
|
data.sorters[0].sortOptions = sortColumn.sortOptions;
|
|
952
968
|
break;
|
|
@@ -1312,13 +1328,19 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
1312
1328
|
mods.push("sortable");
|
|
1313
1329
|
let sorter = data.sorters && data.sorters[0];
|
|
1314
1330
|
let sortColumnField = hdwidget.sortField || hdwidget.field;
|
|
1315
|
-
let sortColumnValue = isDefined(hdwidget.sortValue)
|
|
1331
|
+
let sortColumnValue = isDefined(hdwidget.sortValue)
|
|
1332
|
+
? hdwidget.sortValue
|
|
1333
|
+
: hdwidget.sortField
|
|
1334
|
+
? undefined
|
|
1335
|
+
: hdwidget.value;
|
|
1316
1336
|
// a sort is identified by its (field, value selector) pair, so columns
|
|
1317
1337
|
// sorting by the same field through different value selectors don't both match
|
|
1318
1338
|
let sorted =
|
|
1319
1339
|
sorter &&
|
|
1320
1340
|
!!sorter.direction &&
|
|
1321
|
-
(sortColumnField
|
|
1341
|
+
(sortColumnField
|
|
1342
|
+
? sorter.field == sortColumnField
|
|
1343
|
+
: !sorter.field && isDefined(sortColumnValue)) &&
|
|
1322
1344
|
sorter.value === sortColumnValue;
|
|
1323
1345
|
if (sorted) {
|
|
1324
1346
|
mods.push("sorted-" + sorter.direction.toLowerCase());
|
|
@@ -1494,7 +1516,9 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
1494
1516
|
let header = column.components[`header${headerLine + 1}`];
|
|
1495
1517
|
|
|
1496
1518
|
let field = column.sortField || column.field;
|
|
1497
|
-
|
|
1519
|
+
// precedence: sortValue > sortField > value > field; the comparer prefers value
|
|
1520
|
+
// over field, so value must not be attached when an explicit sortField is set
|
|
1521
|
+
let value = isDefined(column.sortValue) ? column.sortValue : column.sortField ? undefined : column.value;
|
|
1498
1522
|
let comparer = column.comparer;
|
|
1499
1523
|
let sortOptions = column.sortOptions;
|
|
1500
1524
|
|
package/src/widgets/index.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
export { Widget } from "../ui/Widget";
|
|
2
|
-
export { Text } from "../ui/Text";
|
|
3
|
-
export { StaticText } from "../ui/StaticText";
|
|
4
|
-
export { PureContainer } from "../ui/PureContainer";
|
|
5
|
-
export { IsolatedScope } from "../ui/IsolatedScope";
|
|
6
|
-
export { DetachedScope } from "../ui/DetachedScope";
|
|
7
|
-
export { Restate, PrivateStore } from "../ui/Restate";
|
|
8
|
-
export { DataProxy } from "../ui/DataProxy";
|
|
9
|
-
export { Content } from "../ui/layout/Content";
|
|
10
|
-
export { ContentPlaceholder, ContentPlaceholderScope } from "../ui/layout/ContentPlaceholder";
|
|
11
|
-
export { LabelsTopLayout, LabelsTopLayoutCell } from "../ui/layout/LabelsTopLayout";
|
|
12
|
-
export { LabelsLeftLayout } from "../ui/layout/LabelsLeftLayout";
|
|
13
|
-
export { ContentResolver } from "../ui/ContentResolver";
|
|
14
|
-
export { Rescope } from "../ui/Rescope";
|
|
15
|
-
export { Repeater } from "../ui/Repeater";
|
|
16
|
-
|
|
17
|
-
export * from "./cx";
|
|
18
|
-
export * from "./HtmlElement";
|
|
19
|
-
export * from "./Button";
|
|
20
|
-
export * from "./DocumentTitle";
|
|
21
|
-
export * from "./List";
|
|
22
|
-
export * from "./Sandbox";
|
|
23
|
-
export * from "./CxCredit";
|
|
24
|
-
export * from "./Heading";
|
|
25
|
-
export * from "./Section";
|
|
26
|
-
export * from "./FlexBox";
|
|
27
|
-
export * from "./Icon";
|
|
28
|
-
export * from "./ProgressBar";
|
|
29
|
-
export * from "./Resizer";
|
|
30
|
-
export * from "./HighlightedSearchText";
|
|
31
|
-
export * from "./autoFocus";
|
|
32
|
-
export * from "./ReactElementWrapper";
|
|
33
|
-
|
|
34
|
-
export * from "./icons/index";
|
|
35
|
-
export * from "./overlay/index";
|
|
36
|
-
export * from "./nav/index";
|
|
37
|
-
export * from "./form/index";
|
|
38
|
-
export * from "./grid/index";
|
|
39
|
-
export * from "./drag-drop/index";
|
|
40
|
-
|
|
41
|
-
export * from "./enableAllInternalDependencies";
|
|
1
|
+
export { Widget } from "../ui/Widget";
|
|
2
|
+
export { Text } from "../ui/Text";
|
|
3
|
+
export { StaticText } from "../ui/StaticText";
|
|
4
|
+
export { PureContainer } from "../ui/PureContainer";
|
|
5
|
+
export { IsolatedScope } from "../ui/IsolatedScope";
|
|
6
|
+
export { DetachedScope } from "../ui/DetachedScope";
|
|
7
|
+
export { Restate, PrivateStore } from "../ui/Restate";
|
|
8
|
+
export { DataProxy } from "../ui/DataProxy";
|
|
9
|
+
export { Content } from "../ui/layout/Content";
|
|
10
|
+
export { ContentPlaceholder, ContentPlaceholderScope } from "../ui/layout/ContentPlaceholder";
|
|
11
|
+
export { LabelsTopLayout, LabelsTopLayoutCell } from "../ui/layout/LabelsTopLayout";
|
|
12
|
+
export { LabelsLeftLayout } from "../ui/layout/LabelsLeftLayout";
|
|
13
|
+
export { ContentResolver } from "../ui/ContentResolver";
|
|
14
|
+
export { Rescope } from "../ui/Rescope";
|
|
15
|
+
export { Repeater } from "../ui/Repeater";
|
|
16
|
+
|
|
17
|
+
export * from "./cx";
|
|
18
|
+
export * from "./HtmlElement";
|
|
19
|
+
export * from "./Button";
|
|
20
|
+
export * from "./DocumentTitle";
|
|
21
|
+
export * from "./List";
|
|
22
|
+
export * from "./Sandbox";
|
|
23
|
+
export * from "./CxCredit";
|
|
24
|
+
export * from "./Heading";
|
|
25
|
+
export * from "./Section";
|
|
26
|
+
export * from "./FlexBox";
|
|
27
|
+
export * from "./Icon";
|
|
28
|
+
export * from "./ProgressBar";
|
|
29
|
+
export * from "./Resizer";
|
|
30
|
+
export * from "./HighlightedSearchText";
|
|
31
|
+
export * from "./autoFocus";
|
|
32
|
+
export * from "./ReactElementWrapper";
|
|
33
|
+
|
|
34
|
+
export * from "./icons/index";
|
|
35
|
+
export * from "./overlay/index";
|
|
36
|
+
export * from "./nav/index";
|
|
37
|
+
export * from "./form/index";
|
|
38
|
+
export * from "./grid/index";
|
|
39
|
+
export * from "./drag-drop/index";
|
|
40
|
+
|
|
41
|
+
export * from "./enableAllInternalDependencies";
|
|
@@ -385,7 +385,7 @@ class MenuItemComponent extends VDOM.Component<MenuItemComponentProps, MenuItemC
|
|
|
385
385
|
debug(menuFlag, "MenuItem", "mouseLeave", this.el);
|
|
386
386
|
this.clearAutoFocusTimer();
|
|
387
387
|
|
|
388
|
-
if (widget.hoverToOpen &&
|
|
388
|
+
if (widget.hoverToOpen && getActiveElement() == this.el) unfocusElement(this.el!);
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
tooltipMouseLeave(e, this.props.instance, widget.tooltip);
|
|
@@ -486,7 +486,7 @@ class MenuItemComponent extends VDOM.Component<MenuItemComponentProps, MenuItemC
|
|
|
486
486
|
let { widget } = this.props.instance;
|
|
487
487
|
if (widget.dropdown) {
|
|
488
488
|
oneFocusOut(this, this.el!, this.onFocusOut.bind(this));
|
|
489
|
-
debug(menuFlag, "MenuItem", "focus", this.el,
|
|
489
|
+
debug(menuFlag, "MenuItem", "focus", this.el, getActiveElement());
|
|
490
490
|
this.clearAutoFocusTimer();
|
|
491
491
|
if (widget.openOnFocus) this.openDropdown();
|
|
492
492
|
}
|
|
@@ -5,6 +5,7 @@ import { Widget, VDOM } from "../../ui/Widget";
|
|
|
5
5
|
import { calculateNaturalElementHeight } from "../../util/calculateNaturalElementHeight";
|
|
6
6
|
import { closestParent, findFirst, isFocusable } from "../../util/DOM";
|
|
7
7
|
import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect";
|
|
8
|
+
import { getParentFrameBoundingClientRect } from "../../util/getParentFrameBoundingClientRect";
|
|
8
9
|
import { isTouchDevice } from "../../util/isTouchDevice";
|
|
9
10
|
import {
|
|
10
11
|
ConfigureOverlayContainerContext,
|
|
@@ -224,6 +225,23 @@ export class DropdownBase<
|
|
|
224
225
|
component.updateDropdownPosition,
|
|
225
226
|
);
|
|
226
227
|
|
|
228
|
+
//if the related element lives in a different document (e.g. inside a same-origin
|
|
229
|
+
//iframe), that document/window has its own scroll position and its own resize event
|
|
230
|
+
//(which also fires when the <iframe> element itself is resized) - neither is observed
|
|
231
|
+
//by the top-level `window` above or by the relatedElement's parentElement chain, so
|
|
232
|
+
//track every frame window between the related element's document and the top document.
|
|
233
|
+
var frameWindows: Window[] = (component.frameWindows = []);
|
|
234
|
+
var frameWin: Window | null | undefined =
|
|
235
|
+
instance.relatedElement?.ownerDocument?.defaultView;
|
|
236
|
+
while (frameWin && frameWin !== window) {
|
|
237
|
+
frameWindows.push(frameWin);
|
|
238
|
+
frameWin = frameWin.frameElement?.ownerDocument?.defaultView;
|
|
239
|
+
}
|
|
240
|
+
frameWindows.forEach((w) => {
|
|
241
|
+
w.addEventListener("scroll", component.updateDropdownPosition);
|
|
242
|
+
w.addEventListener("resize", component.updateDropdownPosition);
|
|
243
|
+
});
|
|
244
|
+
|
|
227
245
|
if (this.onDropdownDidMount)
|
|
228
246
|
instance.invoke("onDropdownDidMount", instance, component);
|
|
229
247
|
|
|
@@ -246,14 +264,21 @@ export class DropdownBase<
|
|
|
246
264
|
}
|
|
247
265
|
|
|
248
266
|
overlayWillUnmount(instance: InstanceType, component: any): void {
|
|
249
|
-
var { scrollableParents } = component;
|
|
267
|
+
var { scrollableParents, frameWindows } = component;
|
|
250
268
|
if (scrollableParents) {
|
|
251
269
|
scrollableParents.forEach((el: Element) => {
|
|
252
270
|
el.removeEventListener("scroll", component.updateDropdownPosition);
|
|
253
271
|
});
|
|
254
272
|
delete component.scrollableParents;
|
|
255
|
-
delete component.updateDropdownPosition;
|
|
256
273
|
}
|
|
274
|
+
if (frameWindows) {
|
|
275
|
+
frameWindows.forEach((w: Window) => {
|
|
276
|
+
w.removeEventListener("scroll", component.updateDropdownPosition);
|
|
277
|
+
w.removeEventListener("resize", component.updateDropdownPosition);
|
|
278
|
+
});
|
|
279
|
+
delete component.frameWindows;
|
|
280
|
+
}
|
|
281
|
+
delete component.updateDropdownPosition;
|
|
257
282
|
if (component.offResize) component.offResize();
|
|
258
283
|
|
|
259
284
|
if (this.pipeValidateDropdownPosition)
|
|
@@ -278,7 +303,17 @@ export class DropdownBase<
|
|
|
278
303
|
updateDropdownPosition(instance: InstanceType, component: any): void {
|
|
279
304
|
var { el, initialScreenPosition } = component;
|
|
280
305
|
var { data, relatedElement } = instance;
|
|
281
|
-
var
|
|
306
|
+
var elDocument: Document = el.ownerDocument || document;
|
|
307
|
+
|
|
308
|
+
//if the dropdown renders in the same document as its related element (e.g. not
|
|
309
|
+
//portaled to the top-level body, such as an inline dropdown living inside an
|
|
310
|
+
//iframe together with its field), no cross-document offset should be applied -
|
|
311
|
+
//getBoundingClientRect() is already relative to that shared document/viewport.
|
|
312
|
+
var isSameDocument =
|
|
313
|
+
el && relatedElement && el.ownerDocument === relatedElement.ownerDocument;
|
|
314
|
+
var parentBounds = isSameDocument
|
|
315
|
+
? relatedElement!.getBoundingClientRect()
|
|
316
|
+
: getTopLevelBoundingClientRect(relatedElement!);
|
|
282
317
|
|
|
283
318
|
//getBoundingClientRect() will return an empty rect if the element is hidden or removed
|
|
284
319
|
if (
|
|
@@ -291,19 +326,33 @@ export class DropdownBase<
|
|
|
291
326
|
parentBounds = component.parentBounds;
|
|
292
327
|
} else component.parentBounds = parentBounds;
|
|
293
328
|
|
|
294
|
-
|
|
329
|
+
//instance.mousePosition (set from getCursorPos) is always expressed in top-document
|
|
330
|
+
//coordinates. When the dropdown and its related element share a document that isn't
|
|
331
|
+
//the top document (e.g. both live inside the same iframe), parentBounds above is in
|
|
332
|
+
//that document's own coordinate space, so the mouse position must be translated into
|
|
333
|
+
//the same space before it can replace part of parentBounds.
|
|
334
|
+
var mousePosition = instance.mousePosition;
|
|
335
|
+
if (mousePosition && isSameDocument && elDocument !== document) {
|
|
336
|
+
const frameOffset = getParentFrameBoundingClientRect(relatedElement!);
|
|
337
|
+
mousePosition = {
|
|
338
|
+
x: mousePosition.x - frameOffset.left,
|
|
339
|
+
y: mousePosition.y - frameOffset.top,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (this.trackMouseX && mousePosition) {
|
|
295
344
|
parentBounds = new DOMRect(
|
|
296
|
-
|
|
345
|
+
mousePosition.x,
|
|
297
346
|
parentBounds.top,
|
|
298
347
|
0,
|
|
299
348
|
parentBounds.bottom - parentBounds.top,
|
|
300
349
|
);
|
|
301
350
|
}
|
|
302
351
|
|
|
303
|
-
if (this.trackMouseY &&
|
|
352
|
+
if (this.trackMouseY && mousePosition) {
|
|
304
353
|
parentBounds = new DOMRect(
|
|
305
354
|
parentBounds.left,
|
|
306
|
-
|
|
355
|
+
mousePosition.y,
|
|
307
356
|
parentBounds.right - parentBounds.left,
|
|
308
357
|
0,
|
|
309
358
|
);
|
|
@@ -334,6 +383,7 @@ export class DropdownBase<
|
|
|
334
383
|
parentBounds,
|
|
335
384
|
data.placement,
|
|
336
385
|
component.lastPlacement,
|
|
386
|
+
elDocument,
|
|
337
387
|
);
|
|
338
388
|
|
|
339
389
|
var arrowAdjust = this.getArrowAdjust(component);
|
|
@@ -346,6 +396,7 @@ export class DropdownBase<
|
|
|
346
396
|
el,
|
|
347
397
|
false,
|
|
348
398
|
arrowAdjust,
|
|
399
|
+
elDocument,
|
|
349
400
|
);
|
|
350
401
|
component.setCustomStyle(style);
|
|
351
402
|
this.setDirectionClass(component, placement);
|
|
@@ -366,6 +417,7 @@ export class DropdownBase<
|
|
|
366
417
|
el,
|
|
367
418
|
true,
|
|
368
419
|
arrowAdjust,
|
|
420
|
+
elDocument,
|
|
369
421
|
);
|
|
370
422
|
component.setCustomStyle(newStyle);
|
|
371
423
|
}
|
|
@@ -412,8 +464,9 @@ export class DropdownBase<
|
|
|
412
464
|
el: HTMLElement,
|
|
413
465
|
noAuto: boolean,
|
|
414
466
|
arrowAdjust: number = 0,
|
|
467
|
+
elDocument: Document,
|
|
415
468
|
): void {
|
|
416
|
-
let viewport = getViewportRect(this.screenPadding);
|
|
469
|
+
let viewport = getViewportRect(this.screenPadding, elDocument);
|
|
417
470
|
style.position = "fixed";
|
|
418
471
|
|
|
419
472
|
if (placement.startsWith("down")) {
|
|
@@ -431,7 +484,7 @@ export class DropdownBase<
|
|
|
431
484
|
? Math.max(this.screenPadding, top) + "px"
|
|
432
485
|
: "auto";
|
|
433
486
|
style.bottom =
|
|
434
|
-
|
|
487
|
+
elDocument.documentElement.clientHeight -
|
|
435
488
|
(this.cover ? rel.bottom : rel.top) +
|
|
436
489
|
this.offset +
|
|
437
490
|
"px";
|
|
@@ -450,7 +503,7 @@ export class DropdownBase<
|
|
|
450
503
|
break;
|
|
451
504
|
|
|
452
505
|
case "down-left":
|
|
453
|
-
style.right = `${
|
|
506
|
+
style.right = `${elDocument.documentElement.clientWidth - rel.right - arrowAdjust}px`;
|
|
454
507
|
style.left = "auto";
|
|
455
508
|
break;
|
|
456
509
|
|
|
@@ -466,7 +519,7 @@ export class DropdownBase<
|
|
|
466
519
|
break;
|
|
467
520
|
|
|
468
521
|
case "up-left":
|
|
469
|
-
style.right = `${
|
|
522
|
+
style.right = `${elDocument.documentElement.clientWidth - rel.right - arrowAdjust}px`;
|
|
470
523
|
style.left = "auto";
|
|
471
524
|
break;
|
|
472
525
|
|
|
@@ -488,45 +541,45 @@ export class DropdownBase<
|
|
|
488
541
|
case "right-up":
|
|
489
542
|
style.top = "auto";
|
|
490
543
|
style.right = "auto";
|
|
491
|
-
style.bottom = `${
|
|
544
|
+
style.bottom = `${elDocument.documentElement.clientHeight - rel.bottom - arrowAdjust}px`;
|
|
492
545
|
style.left = `${rel.right + this.offset}px`;
|
|
493
546
|
break;
|
|
494
547
|
|
|
495
548
|
case "left":
|
|
496
549
|
case "left-center":
|
|
497
550
|
style.top = `${Math.round((rel.top + rel.bottom - el.offsetHeight) / 2)}px`;
|
|
498
|
-
style.right = `${
|
|
551
|
+
style.right = `${elDocument.documentElement.clientWidth - rel.left + this.offset}px`;
|
|
499
552
|
style.bottom = "auto";
|
|
500
553
|
style.left = "auto";
|
|
501
554
|
break;
|
|
502
555
|
|
|
503
556
|
case "left-down":
|
|
504
557
|
style.top = `${rel.top - arrowAdjust}px`;
|
|
505
|
-
style.right = `${
|
|
558
|
+
style.right = `${elDocument.documentElement.clientWidth - rel.left + this.offset}px`;
|
|
506
559
|
style.bottom = "auto";
|
|
507
560
|
style.left = "auto";
|
|
508
561
|
break;
|
|
509
562
|
|
|
510
563
|
case "left-up":
|
|
511
564
|
style.top = "auto";
|
|
512
|
-
style.right = `${
|
|
513
|
-
style.bottom = `${
|
|
565
|
+
style.right = `${elDocument.documentElement.clientWidth - rel.left + this.offset}px`;
|
|
566
|
+
style.bottom = `${elDocument.documentElement.clientHeight - rel.bottom - arrowAdjust}px`;
|
|
514
567
|
style.left = "auto";
|
|
515
568
|
break;
|
|
516
569
|
|
|
517
570
|
case "screen-center":
|
|
518
571
|
let w = Math.min(
|
|
519
572
|
contentSize.width,
|
|
520
|
-
|
|
573
|
+
elDocument.documentElement.clientWidth - 2 * this.screenPadding,
|
|
521
574
|
);
|
|
522
575
|
let h = Math.min(
|
|
523
576
|
contentSize.height,
|
|
524
|
-
|
|
577
|
+
elDocument.documentElement.clientHeight - 2 * this.screenPadding,
|
|
525
578
|
);
|
|
526
|
-
style.top = `${Math.round((
|
|
527
|
-
style.right = `${Math.round((
|
|
528
|
-
style.bottom = `${Math.round((
|
|
529
|
-
style.left = `${Math.round((
|
|
579
|
+
style.top = `${Math.round((elDocument.documentElement.clientHeight - h) / 2)}px`;
|
|
580
|
+
style.right = `${Math.round((elDocument.documentElement.clientWidth - w) / 2)}px`;
|
|
581
|
+
style.bottom = `${Math.round((elDocument.documentElement.clientHeight - h) / 2)}px`;
|
|
582
|
+
style.left = `${Math.round((elDocument.documentElement.clientWidth - w) / 2)}px`;
|
|
530
583
|
break;
|
|
531
584
|
}
|
|
532
585
|
}
|
|
@@ -539,8 +592,9 @@ export class DropdownBase<
|
|
|
539
592
|
el: HTMLElement,
|
|
540
593
|
noAuto: boolean,
|
|
541
594
|
arrowAdjust: number = 0,
|
|
595
|
+
elDocument: Document,
|
|
542
596
|
): void {
|
|
543
|
-
var viewport = getViewportRect(this.screenPadding);
|
|
597
|
+
var viewport = getViewportRect(this.screenPadding, elDocument);
|
|
544
598
|
|
|
545
599
|
style.position = "absolute";
|
|
546
600
|
|
|
@@ -647,6 +701,7 @@ export class DropdownBase<
|
|
|
647
701
|
el: HTMLElement,
|
|
648
702
|
noAuto: boolean,
|
|
649
703
|
arrowAdjust: number = 0,
|
|
704
|
+
elDocument: Document,
|
|
650
705
|
): void {
|
|
651
706
|
switch (this.positioning) {
|
|
652
707
|
case "absolute":
|
|
@@ -658,6 +713,7 @@ export class DropdownBase<
|
|
|
658
713
|
el,
|
|
659
714
|
noAuto,
|
|
660
715
|
arrowAdjust,
|
|
716
|
+
elDocument,
|
|
661
717
|
);
|
|
662
718
|
break;
|
|
663
719
|
|
|
@@ -671,6 +727,7 @@ export class DropdownBase<
|
|
|
671
727
|
el,
|
|
672
728
|
noAuto,
|
|
673
729
|
arrowAdjust,
|
|
730
|
+
elDocument,
|
|
674
731
|
);
|
|
675
732
|
else
|
|
676
733
|
this.applyFixedPositioningPlacementStyles(
|
|
@@ -681,6 +738,7 @@ export class DropdownBase<
|
|
|
681
738
|
el,
|
|
682
739
|
noAuto,
|
|
683
740
|
arrowAdjust,
|
|
741
|
+
elDocument,
|
|
684
742
|
);
|
|
685
743
|
break;
|
|
686
744
|
|
|
@@ -693,6 +751,7 @@ export class DropdownBase<
|
|
|
693
751
|
el,
|
|
694
752
|
noAuto,
|
|
695
753
|
arrowAdjust,
|
|
754
|
+
elDocument,
|
|
696
755
|
);
|
|
697
756
|
break;
|
|
698
757
|
}
|
|
@@ -749,13 +808,14 @@ export class DropdownBase<
|
|
|
749
808
|
target: any,
|
|
750
809
|
placement: string,
|
|
751
810
|
lastPlacement: any,
|
|
811
|
+
elDocument: Document,
|
|
752
812
|
): any {
|
|
753
813
|
var placementOrder = this.placementOrder.split(" ");
|
|
754
814
|
var best = lastPlacement || placement;
|
|
755
815
|
var first;
|
|
756
816
|
|
|
757
817
|
var score: Record<string, number> = {};
|
|
758
|
-
var viewport = getViewportRect();
|
|
818
|
+
var viewport = getViewportRect(0, elDocument);
|
|
759
819
|
|
|
760
820
|
for (var i = 0; i < placementOrder.length; i++) {
|
|
761
821
|
var p = placementOrder[i];
|
|
@@ -970,11 +1030,11 @@ export class Dropdown extends DropdownBase<DropdownConfig, DropdownInstance> {}
|
|
|
970
1030
|
Widget.alias("dropdown", Dropdown);
|
|
971
1031
|
Localization.registerPrototype("cx/widgets/Dropdown", Dropdown);
|
|
972
1032
|
|
|
973
|
-
function getViewportRect(padding = 0) {
|
|
1033
|
+
function getViewportRect(padding = 0, elDocument: Document = document) {
|
|
974
1034
|
return {
|
|
975
1035
|
left: padding,
|
|
976
1036
|
top: padding,
|
|
977
|
-
right:
|
|
978
|
-
bottom:
|
|
1037
|
+
right: elDocument.documentElement.clientWidth - padding,
|
|
1038
|
+
bottom: elDocument.documentElement.clientHeight - padding,
|
|
979
1039
|
};
|
|
980
1040
|
}
|