cx 26.6.0 → 26.7.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/data/ExposedValueView.js +1 -1
- package/build/data/comparer.d.ts +6 -8
- package/build/data/comparer.d.ts.map +1 -1
- package/build/ui/Prop.d.ts +1 -1
- package/build/ui/Prop.d.ts.map +1 -1
- package/build/ui/adapter/ArrayAdapter.d.ts +4 -6
- package/build/ui/adapter/ArrayAdapter.d.ts.map +1 -1
- package/build/ui/adapter/GroupAdapter.d.ts +22 -2
- package/build/ui/adapter/GroupAdapter.d.ts.map +1 -1
- package/build/ui/adapter/GroupAdapter.js +80 -5
- package/build/widgets/drag-drop/DragSource.d.ts +3 -2
- package/build/widgets/drag-drop/DragSource.d.ts.map +1 -1
- package/build/widgets/drag-drop/DropZone.d.ts +4 -4
- package/build/widgets/drag-drop/DropZone.d.ts.map +1 -1
- package/build/widgets/drag-drop/ops.d.ts +13 -2
- package/build/widgets/drag-drop/ops.d.ts.map +1 -1
- package/build/widgets/drag-drop/ops.js +36 -9
- package/build/widgets/form/NumberField.js +1 -0
- package/build/widgets/grid/Grid.d.ts +19 -3
- package/build/widgets/grid/Grid.d.ts.map +1 -1
- package/build/widgets/grid/Grid.js +2 -0
- package/build/widgets/overlay/captureMouse.d.ts +4 -4
- package/build/widgets/overlay/captureMouse.d.ts.map +1 -1
- package/build/widgets/overlay/captureMouse.js +8 -3
- package/dist/data.js +1 -1
- package/dist/manifest.js +766 -766
- package/dist/ui.js +94 -9
- package/dist/widgets.css +4 -0
- package/dist/widgets.js +42 -10
- package/package.json +1 -1
- package/src/data/ExposedValueView.spec.ts +57 -0
- package/src/data/ExposedValueView.ts +1 -1
- package/src/data/comparer.ts +6 -7
- package/src/ui/Prop.ts +1 -1
- package/src/ui/adapter/ArrayAdapter.ts +6 -8
- package/src/ui/adapter/GroupAdapter.spec.ts +75 -0
- package/src/ui/adapter/GroupAdapter.ts +103 -10
- package/src/widgets/drag-drop/DragSource.tsx +3 -3
- package/src/widgets/drag-drop/DropZone.tsx +5 -5
- package/src/widgets/drag-drop/ops.tsx +54 -12
- package/src/widgets/form/NumberField.scss +4 -0
- package/src/widgets/form/NumberField.tsx +1 -0
- package/src/widgets/grid/Grid.tsx +23 -2
- package/src/widgets/overlay/captureMouse.ts +14 -7
|
@@ -4,11 +4,13 @@ import { ReadOnlyDataView } from "../../data/ReadOnlyDataView";
|
|
|
4
4
|
import { View } from "../../data/View";
|
|
5
5
|
import { isDataRecord } from "../../util";
|
|
6
6
|
import { isArray } from "../../util/isArray";
|
|
7
|
+
import { isDefined } from "../../util/isDefined";
|
|
8
|
+
import { isNonEmptyArray } from "../../util/isNonEmptyArray";
|
|
7
9
|
import { Culture } from "../Culture";
|
|
8
10
|
import { Instance } from "../Instance";
|
|
9
11
|
import { Prop, SortDirection, Sorter, StructuredProp } from "../Prop";
|
|
10
12
|
import { RenderingContext } from "../RenderingContext";
|
|
11
|
-
import { ArrayAdapter, ArrayAdapterConfig, RecordStoreCache } from "./ArrayAdapter";
|
|
13
|
+
import { ArrayAdapter, ArrayAdapterConfig, ExtendedSorter, RecordStoreCache } from "./ArrayAdapter";
|
|
12
14
|
import { DataAdapterRecord } from "./DataAdapter";
|
|
13
15
|
|
|
14
16
|
export interface GroupKey {
|
|
@@ -21,7 +23,17 @@ export interface GroupingConfig {
|
|
|
21
23
|
text?: Prop<string>;
|
|
22
24
|
includeHeader?: boolean;
|
|
23
25
|
includeFooter?: boolean;
|
|
26
|
+
/** Custom group comparer. Takes precedence over `sortField`/`sortDirection`/`sorters`. */
|
|
24
27
|
comparer?: ((a: any, b: any) => number) | null;
|
|
28
|
+
/**
|
|
29
|
+
* Sort groups by a single field resolved against the group's `key`, `aggregates` and `name`.
|
|
30
|
+
* Use an aggregate alias (e.g. `"total"`) to sort by an aggregate, or a key field to sort by key.
|
|
31
|
+
*/
|
|
32
|
+
sortField?: string;
|
|
33
|
+
/** Sort direction used together with `sortField`. Defaults to `"ASC"`. */
|
|
34
|
+
sortDirection?: SortDirection;
|
|
35
|
+
/** Multi-field group sorting. Each sorter's `field` is resolved against the group's `key`, `aggregates` and `name`. */
|
|
36
|
+
sorters?: Sorter[];
|
|
25
37
|
header?: any;
|
|
26
38
|
footer?: any;
|
|
27
39
|
}
|
|
@@ -50,6 +62,8 @@ export interface GroupAdapterConfig extends ArrayAdapterConfig {
|
|
|
50
62
|
groupRecordsName?: string;
|
|
51
63
|
groupings?: GroupingConfig[] | null;
|
|
52
64
|
groupName?: string;
|
|
65
|
+
/** When enabled, the active record sorters also reorder groups (resolved against each group's key/aggregates/name). */
|
|
66
|
+
sortGroupsBySorters?: boolean;
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
export class GroupAdapter<T = any> extends ArrayAdapter<T> {
|
|
@@ -58,11 +72,41 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
|
|
|
58
72
|
declare public groupRecordsName?: string;
|
|
59
73
|
declare public groupings?: ResolvedGrouping[] | null;
|
|
60
74
|
declare public groupName: string;
|
|
75
|
+
declare public sortGroupsBySorters?: boolean;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Per-level comparer derived from the active record sorters, used to reorder groups when
|
|
79
|
+
* `sortGroupsBySorters` is set. A level's entry is `null` when no active sorter maps to that
|
|
80
|
+
* level's key/aggregate, so the grouping's own configured order is kept.
|
|
81
|
+
*/
|
|
82
|
+
protected groupSortComparers?: (((a: any, b: any) => number) | null)[];
|
|
61
83
|
|
|
62
84
|
constructor(config?: GroupAdapterConfig) {
|
|
63
85
|
super(config);
|
|
64
86
|
}
|
|
65
87
|
|
|
88
|
+
public sort(sorters?: ExtendedSorter[]): void {
|
|
89
|
+
super.sort(sorters);
|
|
90
|
+
|
|
91
|
+
// When enabled, derive a group comparer from the active record sorters so that interactive
|
|
92
|
+
// column sorting also reorders the groups. A column only reorders groups at levels where its
|
|
93
|
+
// field is a group key or aggregate; other levels keep their configured order. The record-level
|
|
94
|
+
// value selector is ignored — the field is resolved against the group's key/aggregates/name.
|
|
95
|
+
if (this.sortGroupsBySorters && this.groupings) {
|
|
96
|
+
const cultureComparer = this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined;
|
|
97
|
+
const colSorters: ExtendedSorter[] = isNonEmptyArray(sorters)
|
|
98
|
+
? sorters.map((s) => ({ field: s.field, direction: s.direction, comparer: s.comparer }))
|
|
99
|
+
: [];
|
|
100
|
+
|
|
101
|
+
this.groupSortComparers = this.groupings.map((g) => {
|
|
102
|
+
if (colSorters.length === 0) return null;
|
|
103
|
+
const fields = groupFieldNames(g, this.aggregates);
|
|
104
|
+
const applicable = colSorters.filter((s) => s.field && fields.has(s.field));
|
|
105
|
+
return applicable.length > 0 ? buildGroupComparer(applicable, cultureComparer) : null;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
66
110
|
public init(): void {
|
|
67
111
|
super.init();
|
|
68
112
|
|
|
@@ -116,8 +160,13 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
|
|
|
116
160
|
grouper.processAll(records);
|
|
117
161
|
let results = grouper.getResults();
|
|
118
162
|
|
|
119
|
-
|
|
120
|
-
|
|
163
|
+
// An active column sort that maps to this level's key/aggregate (when `sortGroupsBySorters` is
|
|
164
|
+
// enabled) takes over; otherwise the grouping's configured comparer (or implicit key order) applies.
|
|
165
|
+
const dynamicComparer = this.sortGroupsBySorters ? this.groupSortComparers?.[level] : undefined;
|
|
166
|
+
const comparer = dynamicComparer ?? grouping.comparer;
|
|
167
|
+
|
|
168
|
+
if (comparer && !this.preserveOrder) {
|
|
169
|
+
results.sort(comparer);
|
|
121
170
|
}
|
|
122
171
|
|
|
123
172
|
results.forEach((gr) => {
|
|
@@ -208,15 +257,22 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
|
|
|
208
257
|
g.text,
|
|
209
258
|
);
|
|
210
259
|
|
|
260
|
+
const cultureComparer = this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined;
|
|
261
|
+
|
|
262
|
+
// Sort groups by an aggregate/key/name through `sortField`/`sortDirection` or a `sorters` array.
|
|
263
|
+
let sortSorters: Sorter[] | null = null;
|
|
264
|
+
if (isNonEmptyArray(g.sorters)) sortSorters = g.sorters!;
|
|
265
|
+
else if (g.sortField) sortSorters = [{ field: g.sortField, direction: g.sortDirection ?? "ASC" }];
|
|
266
|
+
|
|
267
|
+
// `comparer`, `sortField`/`sorters` and the implicit key order are equivalent alternatives; an
|
|
268
|
+
// explicit `comparer` wins, then declarative sorters, then sorting by key in declaration order.
|
|
211
269
|
const comparer =
|
|
212
270
|
g.comparer ??
|
|
213
|
-
(
|
|
214
|
-
?
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
)
|
|
219
|
-
: null);
|
|
271
|
+
(sortSorters
|
|
272
|
+
? buildGroupComparer(sortSorters, cultureComparer)
|
|
273
|
+
: groupSorters.length > 0
|
|
274
|
+
? getComparer(groupSorters, (x: any) => x.key, cultureComparer)
|
|
275
|
+
: null);
|
|
220
276
|
|
|
221
277
|
return {
|
|
222
278
|
...g,
|
|
@@ -233,6 +289,43 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
|
|
|
233
289
|
|
|
234
290
|
GroupAdapter.prototype.groupName = "$group";
|
|
235
291
|
GroupAdapter.prototype.preserveOrder = false;
|
|
292
|
+
GroupAdapter.prototype.sortGroupsBySorters = false;
|
|
293
|
+
|
|
294
|
+
// The set of fields a group can be sorted by at a given level: its key fields, its aggregate aliases
|
|
295
|
+
// and the group name. Used to decide whether an active column sort applies to that grouping level.
|
|
296
|
+
function groupFieldNames(grouping: ResolvedGrouping, adapterAggregates?: StructuredProp): Set<string> {
|
|
297
|
+
const names = new Set<string>();
|
|
298
|
+
for (const k of grouping.grouper.keys) names.add(k.name);
|
|
299
|
+
const aggregates = { ...adapterAggregates, ...grouping.aggregates };
|
|
300
|
+
for (const a in aggregates) names.add(a);
|
|
301
|
+
names.add("name");
|
|
302
|
+
names.add("$name");
|
|
303
|
+
return names;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Reads a sort field straight from the GroupResult — first its key fields, then its aggregates, then
|
|
307
|
+
// its name. Returns a plain selector so groups can be sorted without cloning the result per comparison.
|
|
308
|
+
function groupFieldSelector(field: string): (gr: any) => any {
|
|
309
|
+
if (field === "name" || field === "$name") return (gr) => gr?.name;
|
|
310
|
+
return (gr) => {
|
|
311
|
+
if (gr?.key && field in gr.key) return gr.key[field];
|
|
312
|
+
if (gr?.aggregates && field in gr.aggregates) return gr.aggregates[field];
|
|
313
|
+
return undefined;
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Builds a group comparer from a list of sorters. Each sorter resolves by its explicit `value`
|
|
318
|
+
// selector, or by `field` looked up against the group's key/aggregates/name.
|
|
319
|
+
function buildGroupComparer(
|
|
320
|
+
sorters: ExtendedSorter[],
|
|
321
|
+
cultureComparer?: (a: any, b: any) => number,
|
|
322
|
+
): (a: any, b: any) => number {
|
|
323
|
+
return getComparer(
|
|
324
|
+
sorters.map((s) => (isDefined(s.value) || !s.field ? s : { ...s, value: groupFieldSelector(s.field) })),
|
|
325
|
+
undefined,
|
|
326
|
+
cultureComparer,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
236
329
|
|
|
237
330
|
function serializeKey(data: any): string {
|
|
238
331
|
if (isDataRecord(data)) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Widget, VDOM } from "../../ui/Widget";
|
|
4
4
|
import { ContainerBase, ContainerConfig, StyledContainerBase, StyledContainerConfig } from "../../ui/Container";
|
|
5
|
-
import { ddMouseDown, ddDetect, ddMouseUp, initiateDragDrop, isDragHandleEvent } from "./ops";
|
|
5
|
+
import { ddMouseDown, ddDetect, ddMouseUp, initiateDragDrop, isDragHandleEvent, DragEndEvent } from "./ops";
|
|
6
6
|
import { preventFocus } from "../../ui/FocusManager";
|
|
7
7
|
import { parseStyle } from "../../util/parseStyle";
|
|
8
8
|
import { Instance } from "../../ui/Instance";
|
|
@@ -30,7 +30,7 @@ export interface DragSourceConfig extends StyledContainerConfig {
|
|
|
30
30
|
|
|
31
31
|
onDragStart?: (e: React.MouseEvent | React.TouchEvent, instance: Instance) => any;
|
|
32
32
|
|
|
33
|
-
onDragEnd?: (e:
|
|
33
|
+
onDragEnd?: (e: DragEndEvent, instance: Instance) => void;
|
|
34
34
|
|
|
35
35
|
id?: StringProp;
|
|
36
36
|
|
|
@@ -64,7 +64,7 @@ export class DragSource extends StyledContainerBase<DragSourceConfig, DragSource
|
|
|
64
64
|
declare cloneStyle: any;
|
|
65
65
|
declare draggedStyle: any;
|
|
66
66
|
declare onDragStart?: (e: React.MouseEvent | React.TouchEvent, instance: DragSourceInstance) => any;
|
|
67
|
-
declare onDragEnd?: (e:
|
|
67
|
+
declare onDragEnd?: (e: DragEndEvent, instance: DragSourceInstance) => void;
|
|
68
68
|
|
|
69
69
|
constructor(config?: DragSourceConfig) {
|
|
70
70
|
super(config);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Widget, VDOM } from "../../ui/Widget";
|
|
4
4
|
import { ContainerBase, ContainerConfig, StyledContainerBase, StyledContainerConfig } from "../../ui/Container";
|
|
5
5
|
import { parseStyle } from "../../util/parseStyle";
|
|
6
|
-
import { registerDropZone, DragDropContext, DragEvent } from "./ops";
|
|
6
|
+
import { registerDropZone, DragDropContext, DragEvent, DragEndEvent } from "./ops";
|
|
7
7
|
import { findScrollableParent } from "../../util/findScrollableParent";
|
|
8
8
|
import { isNumber } from "../../util/isNumber";
|
|
9
9
|
import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect";
|
|
@@ -90,8 +90,8 @@ export interface DropZoneConfig extends StyledContainerConfig {
|
|
|
90
90
|
/** A callback method invoked when at the beginning of the drag & drop operation. */
|
|
91
91
|
onDragStart?: string | ((event: DragEvent, instance: Instance) => void);
|
|
92
92
|
|
|
93
|
-
/** A callback method invoked when at the end of the drag & drop operation. */
|
|
94
|
-
onDragEnd?: string | ((event:
|
|
93
|
+
/** A callback method invoked when at the end of the drag & drop operation. `event.cancelled` is `true` when the operation was cancelled (e.g. via Esc) instead of dropped. */
|
|
94
|
+
onDragEnd?: string | ((event: DragEndEvent, instance: Instance) => void);
|
|
95
95
|
|
|
96
96
|
/** Match height of the item being dragged */
|
|
97
97
|
matchHeight?: boolean;
|
|
@@ -126,7 +126,7 @@ export class DropZone extends StyledContainerBase<DropZoneConfig, DropZoneInstan
|
|
|
126
126
|
declare onDragEnter?: string | ((event?: DragEvent, instance?: Instance) => void);
|
|
127
127
|
declare onDragLeave?: string | ((event?: DragEvent, instance?: Instance) => void);
|
|
128
128
|
declare onDragStart?: string | ((event?: DragEvent, instance?: Instance) => void);
|
|
129
|
-
declare onDragEnd?: string | ((event?:
|
|
129
|
+
declare onDragEnd?: string | ((event?: DragEndEvent, instance?: Instance) => void);
|
|
130
130
|
|
|
131
131
|
constructor(config?: DropZoneConfig) {
|
|
132
132
|
super(config);
|
|
@@ -342,7 +342,7 @@ class DropZoneComponent extends VDOM.Component<DropZoneComponentProps, DropZoneC
|
|
|
342
342
|
if (this.state.state == "over" && widget.onDrop) instance.invoke("onDrop", e, instance);
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
onDragEnd(e:
|
|
345
|
+
onDragEnd(e: DragEndEvent) {
|
|
346
346
|
this.setState({
|
|
347
347
|
state: false,
|
|
348
348
|
style: null,
|
|
@@ -21,6 +21,18 @@ export interface DragEvent {
|
|
|
21
21
|
result?: any;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface DragEndEvent {
|
|
25
|
+
type: "dragend";
|
|
26
|
+
/** The triggering pointer event, or `null` when the operation was cancelled via the keyboard. */
|
|
27
|
+
event: DragEvent["event"] | null;
|
|
28
|
+
/** Cursor position, or `null` when the operation was cancelled via the keyboard. */
|
|
29
|
+
cursor: CursorPosition | null;
|
|
30
|
+
source: DragEvent["source"];
|
|
31
|
+
/** `true` when the operation was cancelled (e.g. by pressing Esc) instead of dropped. */
|
|
32
|
+
cancelled: boolean;
|
|
33
|
+
result?: any;
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export interface DragDropOptions {
|
|
25
37
|
sourceEl?: Element | null;
|
|
26
38
|
clone?: any;
|
|
@@ -38,7 +50,7 @@ export interface IDropZone {
|
|
|
38
50
|
onDragStart?: DragEventHandler;
|
|
39
51
|
onDragAway?: DragEventHandler;
|
|
40
52
|
onDragNear?: DragEventHandler;
|
|
41
|
-
onDragEnd?:
|
|
53
|
+
onDragEnd?: (e: DragEndEvent) => void;
|
|
42
54
|
onDragMeasure?: (
|
|
43
55
|
e: DragEvent,
|
|
44
56
|
operation: DragDropOperationContext,
|
|
@@ -62,7 +74,6 @@ import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingCli
|
|
|
62
74
|
import { VDOM } from "../../ui/VDOM";
|
|
63
75
|
import { Container } from "../../ui/Container";
|
|
64
76
|
import { Console } from "../../util";
|
|
65
|
-
import { WidgetConfig } from "../../ui";
|
|
66
77
|
|
|
67
78
|
interface Puppet {
|
|
68
79
|
deltaX: number;
|
|
@@ -70,7 +81,7 @@ interface Puppet {
|
|
|
70
81
|
el: HTMLDivElement;
|
|
71
82
|
clone: any;
|
|
72
83
|
source: any;
|
|
73
|
-
onDragEnd?: (e?:
|
|
84
|
+
onDragEnd?: (e?: DragEndEvent) => void;
|
|
74
85
|
stop?: () => void;
|
|
75
86
|
}
|
|
76
87
|
|
|
@@ -82,6 +93,7 @@ let puppet: Puppet | null = null;
|
|
|
82
93
|
let scrollTimer: number | null = null;
|
|
83
94
|
let vscrollParent: Element | null = null;
|
|
84
95
|
let hscrollParent: Element | null = null;
|
|
96
|
+
let releaseCapture: (() => void) | null = null;
|
|
85
97
|
|
|
86
98
|
export function registerDropZone(dropZone: IDropZone): UnregisterFunction {
|
|
87
99
|
dropZones.push(dropZone);
|
|
@@ -94,7 +106,7 @@ export function registerDropZone(dropZone: IDropZone): UnregisterFunction {
|
|
|
94
106
|
export function initiateDragDrop(
|
|
95
107
|
e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent,
|
|
96
108
|
options: DragDropOptions = {},
|
|
97
|
-
onDragEnd?: (e?:
|
|
109
|
+
onDragEnd?: (e?: DragEndEvent) => void,
|
|
98
110
|
): void {
|
|
99
111
|
if (puppet) {
|
|
100
112
|
//last operation didn't finish properly
|
|
@@ -187,7 +199,16 @@ export function initiateDragDrop(
|
|
|
187
199
|
|
|
188
200
|
notifyDragMove(e, null);
|
|
189
201
|
|
|
190
|
-
captureMouseOrTouch(e, notifyDragMove, notifyDragDrop);
|
|
202
|
+
releaseCapture = captureMouseOrTouch(e, notifyDragMove, notifyDragDrop);
|
|
203
|
+
document.addEventListener("keydown", onDragKeyDown, true);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function onDragKeyDown(e: KeyboardEvent): void {
|
|
207
|
+
if (!puppet || e.key !== "Escape") return;
|
|
208
|
+
//cancel the operation without dropping; there is no pointer event to report
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
e.stopPropagation();
|
|
211
|
+
notifyDragDrop(null, true);
|
|
191
212
|
}
|
|
192
213
|
|
|
193
214
|
function notifyDragMove(e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent, _captureData: any): void {
|
|
@@ -321,29 +342,50 @@ function clearScrollTimer() {
|
|
|
321
342
|
}
|
|
322
343
|
}
|
|
323
344
|
|
|
324
|
-
function notifyDragDrop(
|
|
345
|
+
function notifyDragDrop(
|
|
346
|
+
e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent | null,
|
|
347
|
+
cancelled: boolean = false,
|
|
348
|
+
): void {
|
|
349
|
+
if (!puppet) return;
|
|
350
|
+
|
|
325
351
|
clearScrollTimer();
|
|
352
|
+
document.removeEventListener("keydown", onDragKeyDown, true);
|
|
326
353
|
|
|
327
|
-
|
|
354
|
+
if (puppet.stop) puppet.stop();
|
|
328
355
|
|
|
329
|
-
|
|
356
|
+
//a keyboard cancellation has no pointer event, so the drop and away notifications are skipped
|
|
357
|
+
let dropEvent = !cancelled && e ? getDragEvent(e, "dragdrop") : null;
|
|
330
358
|
|
|
331
|
-
|
|
359
|
+
let result: any;
|
|
360
|
+
if (dropEvent && activeZone && activeZone.onDrop) result = activeZone.onDrop(dropEvent);
|
|
361
|
+
|
|
362
|
+
let endEvent: DragEndEvent = {
|
|
363
|
+
type: "dragend",
|
|
364
|
+
event: e,
|
|
365
|
+
cursor: e ? getCursorPos(e) : null,
|
|
366
|
+
source: puppet.source,
|
|
367
|
+
cancelled,
|
|
368
|
+
result,
|
|
369
|
+
};
|
|
332
370
|
|
|
333
371
|
dropZones.forEach((zone) => {
|
|
334
|
-
if (nearZones != null && zone.onDragAway && nearZones.has(zone)) zone.onDragAway(
|
|
372
|
+
if (dropEvent && nearZones != null && zone.onDragAway && nearZones.has(zone)) zone.onDragAway(dropEvent);
|
|
335
373
|
|
|
336
374
|
if (!dragStartedZones!.has(zone)) return;
|
|
337
375
|
|
|
338
|
-
if (zone.onDragEnd) zone.onDragEnd(
|
|
376
|
+
if (zone.onDragEnd) zone.onDragEnd(endEvent);
|
|
339
377
|
});
|
|
340
378
|
|
|
341
|
-
if (puppet
|
|
379
|
+
if (puppet.onDragEnd) puppet.onDragEnd(endEvent);
|
|
380
|
+
|
|
381
|
+
//tear down the mouse/touch capture when cancelling; on a normal drop the capture ends itself
|
|
382
|
+
if (cancelled && releaseCapture) releaseCapture();
|
|
342
383
|
|
|
343
384
|
nearZones = null;
|
|
344
385
|
activeZone = null;
|
|
345
386
|
puppet = null;
|
|
346
387
|
dragStartedZones = null;
|
|
388
|
+
releaseCapture = null;
|
|
347
389
|
}
|
|
348
390
|
|
|
349
391
|
function getDragEvent(
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
.#{$state}icon > & {
|
|
43
43
|
padding-left: cx-calc(cx-top($padding), $cx-default-input-left-tool-size, $cx-default-input-left-tool-spacing);
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
.#{$state}clear > & {
|
|
47
|
+
padding-right: cx-calc(cx-top($padding), $cx-default-clear-size, $cx-default-clear-spacing);
|
|
48
|
+
}
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
.#{$element}#{$name}-tool {
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
NumberProp,
|
|
24
24
|
Prop,
|
|
25
25
|
RecordAlias,
|
|
26
|
+
Sorter,
|
|
26
27
|
SortDirection,
|
|
27
28
|
SortersProp,
|
|
28
29
|
StringProp,
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
ddMouseDown,
|
|
63
64
|
DragDropOperationContext,
|
|
64
65
|
DragEvent,
|
|
66
|
+
DragEndEvent,
|
|
65
67
|
initiateDragDrop,
|
|
66
68
|
registerDropZone,
|
|
67
69
|
} from "../drag-drop/ops";
|
|
@@ -165,6 +167,15 @@ export interface GridGroupingConfig<T> {
|
|
|
165
167
|
name?: StringProp;
|
|
166
168
|
text?: StringProp;
|
|
167
169
|
comparer?: (a: GroupingResult<T>, b: GroupingResult<T>) => number;
|
|
170
|
+
/**
|
|
171
|
+
* Sort groups by a single field resolved against the group's key, aggregates and name.
|
|
172
|
+
* Use an aggregate alias to sort by an aggregate, or a key field to sort by key.
|
|
173
|
+
*/
|
|
174
|
+
sortField?: string;
|
|
175
|
+
/** Sort direction used together with `sortField`. Defaults to `"ASC"`. */
|
|
176
|
+
sortDirection?: SortDirection;
|
|
177
|
+
/** Multi-field group sorting. Each sorter's `field` is resolved against the group's key, aggregates and name. */
|
|
178
|
+
sorters?: Sorter[];
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
export interface GridColumnHeaderConfig {
|
|
@@ -390,7 +401,7 @@ export interface GridConfig<T = any> extends StyledContainerConfig {
|
|
|
390
401
|
onDrop?: string | ((e: GridDragEvent<T>, instance: Instance) => void);
|
|
391
402
|
onDropTest?: string | ((e: DragEvent, instance: Instance) => boolean);
|
|
392
403
|
onDragStart?: string | ((e: DragEvent, instance: Instance) => void);
|
|
393
|
-
onDragEnd?: string | ((e:
|
|
404
|
+
onDragEnd?: string | ((e: DragEndEvent, instance: Instance) => void);
|
|
394
405
|
onDragOver?: string | ((e: GridDragEvent<T>, instance: Instance) => void | boolean);
|
|
395
406
|
|
|
396
407
|
onRowDropTest?: string | ((e: DragEvent, instance: Instance) => boolean);
|
|
@@ -502,6 +513,13 @@ export interface GridConfig<T = any> extends StyledContainerConfig {
|
|
|
502
513
|
|
|
503
514
|
/** When enabled, groups are shown in the same order as the source records. */
|
|
504
515
|
preserveGroupOrder?: boolean;
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* When enabled, sorting by a column header also reorders groups. The active sort field is resolved
|
|
519
|
+
* against each group's key fields, aggregates and name, so it works for both grouping columns and
|
|
520
|
+
* aggregate columns. Ignored when `preserveGroupOrder` is set.
|
|
521
|
+
*/
|
|
522
|
+
sortGroups?: boolean;
|
|
505
523
|
}
|
|
506
524
|
|
|
507
525
|
export interface GridCellInfo {
|
|
@@ -635,6 +653,7 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
635
653
|
declare onCreateIsRecordDraggable?: any;
|
|
636
654
|
declare onRef?: any;
|
|
637
655
|
declare preserveGroupOrder: boolean;
|
|
656
|
+
declare sortGroups: boolean;
|
|
638
657
|
declare styled: boolean;
|
|
639
658
|
declare selectable?: boolean;
|
|
640
659
|
declare recordsAccessor: any;
|
|
@@ -844,6 +863,7 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
844
863
|
sortOptions: this.sortOptions,
|
|
845
864
|
groupings: grouping,
|
|
846
865
|
preserveOrder: this.preserveGroupOrder,
|
|
866
|
+
sortGroupsBySorters: this.sortGroups,
|
|
847
867
|
},
|
|
848
868
|
this.dataAdapter,
|
|
849
869
|
);
|
|
@@ -1903,6 +1923,7 @@ Grid.prototype.hoverChannel = "default";
|
|
|
1903
1923
|
Grid.prototype.focusable = null; // automatically resolved
|
|
1904
1924
|
Grid.prototype.allowsFileDrops = false;
|
|
1905
1925
|
Grid.prototype.preserveGroupOrder = false;
|
|
1926
|
+
Grid.prototype.sortGroups = false;
|
|
1906
1927
|
|
|
1907
1928
|
Widget.alias("grid", Grid);
|
|
1908
1929
|
Localization.registerPrototype("cx/widgets/Grid", Grid);
|
|
@@ -2851,7 +2872,7 @@ class GridComponent extends VDOM.Component<GridComponentProps, GridComponentStat
|
|
|
2851
2872
|
return (grid || row || column) && { grid, row, column };
|
|
2852
2873
|
}
|
|
2853
2874
|
|
|
2854
|
-
onDragEnd(e:
|
|
2875
|
+
onDragEnd(e: DragEndEvent) {
|
|
2855
2876
|
this.setState({
|
|
2856
2877
|
dropTarget: null,
|
|
2857
2878
|
dropInsertionIndex: null,
|
|
@@ -31,7 +31,7 @@ interface CaptureMouseOptions {
|
|
|
31
31
|
export function captureMouse2(
|
|
32
32
|
e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent,
|
|
33
33
|
options: CaptureMouseOptions,
|
|
34
|
-
): void {
|
|
34
|
+
): () => void {
|
|
35
35
|
let surface = document.createElement("div");
|
|
36
36
|
surface.className = "cxb-mousecapture";
|
|
37
37
|
surface.style.cursor = options.cursor || getComputedStyle(e.currentTarget as Element).cursor;
|
|
@@ -67,6 +67,8 @@ export function captureMouse2(
|
|
|
67
67
|
|
|
68
68
|
e.stopPropagation();
|
|
69
69
|
|
|
70
|
+
return tear;
|
|
71
|
+
|
|
70
72
|
function move(e: Event) {
|
|
71
73
|
if (!active) {
|
|
72
74
|
tear();
|
|
@@ -110,7 +112,7 @@ export function captureMouse2(
|
|
|
110
112
|
export function captureMouseOrTouch2(
|
|
111
113
|
e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent,
|
|
112
114
|
options: CaptureMouseOptions,
|
|
113
|
-
): void {
|
|
115
|
+
): () => void {
|
|
114
116
|
if (e.type.indexOf("touch") == 0) {
|
|
115
117
|
let el = e.currentTarget as HTMLElement;
|
|
116
118
|
|
|
@@ -136,7 +138,12 @@ export function captureMouseOrTouch2(
|
|
|
136
138
|
el.addEventListener("touchend", end);
|
|
137
139
|
|
|
138
140
|
e.stopPropagation();
|
|
139
|
-
|
|
141
|
+
|
|
142
|
+
return () => {
|
|
143
|
+
el.removeEventListener("touchmove", move);
|
|
144
|
+
el.removeEventListener("touchend", end);
|
|
145
|
+
};
|
|
146
|
+
} else return captureMouse2(e, options);
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
/**
|
|
@@ -153,8 +160,8 @@ export function captureMouse(
|
|
|
153
160
|
onMouseUp?: (e: MouseEvent | TouchEvent, captureData?: any) => void,
|
|
154
161
|
captureData?: any,
|
|
155
162
|
cursor?: string,
|
|
156
|
-
): void {
|
|
157
|
-
captureMouse2(e, {
|
|
163
|
+
): () => void {
|
|
164
|
+
return captureMouse2(e, {
|
|
158
165
|
onMouseMove,
|
|
159
166
|
onMouseUp,
|
|
160
167
|
captureData,
|
|
@@ -176,8 +183,8 @@ export function captureMouseOrTouch(
|
|
|
176
183
|
onMouseUp?: (e: MouseEvent, captureData?: any) => void,
|
|
177
184
|
captureData?: any,
|
|
178
185
|
cursor?: string,
|
|
179
|
-
): void {
|
|
180
|
-
captureMouseOrTouch2(e, { onMouseMove, onMouseUp, captureData, cursor });
|
|
186
|
+
): () => void {
|
|
187
|
+
return captureMouseOrTouch2(e, { onMouseMove, onMouseUp, captureData, cursor });
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
/**
|