@kanaries/graphic-walker 0.2.9 → 0.2.10
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/dist/App.d.ts +0 -2
- package/dist/components/callout.d.ts +7 -0
- package/dist/components/sizeSetting.d.ts +1 -0
- package/dist/components/toolbar/components.d.ts +11 -0
- package/dist/components/toolbar/index.d.ts +15 -0
- package/dist/components/toolbar/toolbar-button.d.ts +7 -0
- package/dist/components/toolbar/toolbar-item.d.ts +40 -0
- package/dist/components/toolbar/toolbar-select-button.d.ts +18 -0
- package/dist/components/toolbar/toolbar-toggle-button.d.ts +8 -0
- package/dist/components/tooltip.d.ts +13 -0
- package/dist/fields/datasetFields/dimFields.d.ts +1 -1
- package/dist/fields/datasetFields/meaFields.d.ts +1 -1
- package/dist/fields/filterField/filterPill.d.ts +1 -1
- package/dist/fields/obComponents/obFContainer.d.ts +1 -1
- package/dist/fields/obComponents/obPill.d.ts +1 -1
- package/dist/graphic-walker.es.js +56487 -55304
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +501 -288
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/style.css +0 -1
- package/dist/utils/throttle.d.ts +5 -0
- package/dist/visualSettings/menubar.d.ts +8 -0
- package/package.json +4 -2
- package/src/App.tsx +0 -4
- package/src/components/callout.tsx +58 -0
- package/src/components/sizeSetting.tsx +61 -49
- package/src/components/tabs/pureTab.tsx +7 -1
- package/src/components/toolbar/components.tsx +110 -0
- package/src/components/toolbar/index.tsx +57 -0
- package/src/components/toolbar/toolbar-button.tsx +28 -0
- package/src/components/toolbar/toolbar-item.tsx +218 -0
- package/src/components/toolbar/toolbar-select-button.tsx +196 -0
- package/src/components/toolbar/toolbar-toggle-button.tsx +70 -0
- package/src/components/tooltip.tsx +135 -0
- package/src/empty_sheet.css +9 -0
- package/src/fields/aestheticFields.tsx +1 -1
- package/src/fields/datasetFields/dimFields.tsx +3 -3
- package/src/fields/datasetFields/index.tsx +2 -2
- package/src/fields/datasetFields/meaFields.tsx +3 -3
- package/src/fields/fieldsContext.tsx +1 -1
- package/src/fields/filterField/filterPill.tsx +1 -1
- package/src/fields/filterField/index.tsx +1 -1
- package/src/fields/obComponents/obFContainer.tsx +1 -1
- package/src/fields/obComponents/obPill.tsx +1 -1
- package/src/fields/posFields/index.tsx +1 -1
- package/src/global.d.ts +7 -0
- package/src/index.tsx +47 -8
- package/src/store/visualSpecStore.ts +1 -1
- package/src/utils/throttle.ts +28 -0
- package/src/visualSettings/index.tsx +316 -321
- package/src/visualSettings/menubar.tsx +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { ChevronDownIcon, Cog6ToothIcon, Cog8ToothIcon } from "@heroicons/react/24/solid";
|
|
2
|
+
import React, { HTMLAttributes, memo, ReactNode, useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import styled from "styled-components";
|
|
4
|
+
import ToolbarButton, { ToolbarButtonItem } from "./toolbar-button";
|
|
5
|
+
import ToolbarToggleButton, { ToolbarToggleButtonItem } from "./toolbar-toggle-button";
|
|
6
|
+
import ToolbarSelectButton, { ToolbarSelectButtonItem } from "./toolbar-select-button";
|
|
7
|
+
import { ToolbarContainer, ToolbarItemContainerElement, ToolbarSplitter, useHandlers } from "./components";
|
|
8
|
+
import Toolbar, { ToolbarProps } from ".";
|
|
9
|
+
import Tooltip from "../tooltip";
|
|
10
|
+
import Callout from "../callout";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const ToolbarSplit = styled.div<{ open: boolean }>`
|
|
14
|
+
flex-grow: 1;
|
|
15
|
+
flex-shrink: 1;
|
|
16
|
+
display: inline-block;
|
|
17
|
+
height: var(--height);
|
|
18
|
+
position: relative;
|
|
19
|
+
margin-right: 4px;
|
|
20
|
+
> svg {
|
|
21
|
+
position: absolute;
|
|
22
|
+
width: calc(var(--icon-size) * 0.6);
|
|
23
|
+
height: calc(var(--icon-size) * 0.6);
|
|
24
|
+
left: 50%;
|
|
25
|
+
top: 50%;
|
|
26
|
+
transform: translate(-50%, ${({ open }) => open ? '-20%' : '-50%'});
|
|
27
|
+
transition: transform 120ms;
|
|
28
|
+
}
|
|
29
|
+
:hover > svg, :focus > svg {
|
|
30
|
+
transform: translate(-50%, -20%);
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const FormContainer = styled(ToolbarContainer)`
|
|
35
|
+
width: max-content;
|
|
36
|
+
height: max-content;
|
|
37
|
+
background-color: #fff;
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export interface IToolbarItem {
|
|
41
|
+
key: string;
|
|
42
|
+
icon: (props: React.ComponentProps<'svg'> & {
|
|
43
|
+
title?: string;
|
|
44
|
+
titleId?: string;
|
|
45
|
+
}) => JSX.Element;
|
|
46
|
+
label: string;
|
|
47
|
+
/** @default false */
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
menu?: ToolbarProps;
|
|
50
|
+
form?: JSX.Element;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ToolbarItemSplitter = '-';
|
|
54
|
+
|
|
55
|
+
export type ToolbarItemProps = (
|
|
56
|
+
| ToolbarButtonItem
|
|
57
|
+
| ToolbarToggleButtonItem
|
|
58
|
+
| ToolbarSelectButtonItem
|
|
59
|
+
| typeof ToolbarItemSplitter
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export interface IToolbarProps<P extends Exclude<ToolbarItemProps, typeof ToolbarItemSplitter> = Exclude<ToolbarItemProps, typeof ToolbarItemSplitter>> {
|
|
63
|
+
item: P;
|
|
64
|
+
styles?: ToolbarProps['styles'];
|
|
65
|
+
openedKey: string | null;
|
|
66
|
+
setOpenedKey: (key: string | null) => void;
|
|
67
|
+
renderSlot: (node: ReactNode) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let idFlag = 0;
|
|
71
|
+
|
|
72
|
+
export const ToolbarItemContainer = memo<{
|
|
73
|
+
props: IToolbarProps;
|
|
74
|
+
handlers: ReturnType<typeof useHandlers> | null;
|
|
75
|
+
children: unknown;
|
|
76
|
+
} & HTMLAttributes<HTMLDivElement>>(function ToolbarItemContainer (
|
|
77
|
+
{
|
|
78
|
+
props: {
|
|
79
|
+
item: { key, label, disabled = false, menu, form },
|
|
80
|
+
styles, openedKey, setOpenedKey, renderSlot,
|
|
81
|
+
},
|
|
82
|
+
handlers,
|
|
83
|
+
children,
|
|
84
|
+
...props
|
|
85
|
+
}
|
|
86
|
+
) {
|
|
87
|
+
const id = useMemo(() => `toolbar-item-${idFlag++}`, []);
|
|
88
|
+
const splitOnly = Boolean(form || menu) && handlers === null;
|
|
89
|
+
|
|
90
|
+
const opened = Boolean(form || menu) && key === openedKey && !disabled;
|
|
91
|
+
const openedRef = useRef(opened);
|
|
92
|
+
openedRef.current = opened;
|
|
93
|
+
|
|
94
|
+
const splitHandlers = useHandlers(() => {
|
|
95
|
+
setOpenedKey(opened ? null : key);
|
|
96
|
+
}, disabled ?? false, [' '], false);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (opened) {
|
|
100
|
+
const close = (e?: unknown) => {
|
|
101
|
+
if (!openedRef.current) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!e) {
|
|
105
|
+
setOpenedKey(null);
|
|
106
|
+
} else if (e instanceof KeyboardEvent && e.key === 'Escape') {
|
|
107
|
+
setOpenedKey(null);
|
|
108
|
+
} else if (e instanceof MouseEvent) {
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
if (openedRef.current) {
|
|
111
|
+
setOpenedKey(null);
|
|
112
|
+
}
|
|
113
|
+
}, 100);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
document.addEventListener('mousedown', close);
|
|
118
|
+
document.addEventListener('keydown', close);
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
document.removeEventListener('mousedown', close);
|
|
122
|
+
document.removeEventListener('keydown', close);
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}, [setOpenedKey, opened]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (opened && menu) {
|
|
129
|
+
renderSlot(<Toolbar {...menu} />);
|
|
130
|
+
return () => renderSlot(null);
|
|
131
|
+
}
|
|
132
|
+
}, [opened, menu, renderSlot]);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<>
|
|
136
|
+
<Tooltip content={label}>
|
|
137
|
+
<ToolbarItemContainerElement
|
|
138
|
+
role="button" tabIndex={disabled ? undefined : 0} aria-label={label} aria-disabled={disabled ?? false}
|
|
139
|
+
split={Boolean(form || menu)}
|
|
140
|
+
style={styles?.item}
|
|
141
|
+
className={opened ? 'open' : undefined}
|
|
142
|
+
aria-haspopup={splitOnly ? 'menu' : 'false'}
|
|
143
|
+
{...(splitOnly ? splitHandlers : handlers)}
|
|
144
|
+
{...props}
|
|
145
|
+
id={id}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
{form ? (
|
|
149
|
+
splitOnly ? (
|
|
150
|
+
<ToolbarSplit
|
|
151
|
+
open={opened}
|
|
152
|
+
{...splitHandlers}
|
|
153
|
+
>
|
|
154
|
+
<Cog6ToothIcon style={styles?.splitIcon}/>
|
|
155
|
+
</ToolbarSplit>
|
|
156
|
+
) : (
|
|
157
|
+
<ToolbarSplit
|
|
158
|
+
open={opened}
|
|
159
|
+
role="button"
|
|
160
|
+
tabIndex={disabled ? undefined : 0}
|
|
161
|
+
{...splitHandlers}
|
|
162
|
+
>
|
|
163
|
+
<Cog6ToothIcon style={styles?.splitIcon}/>
|
|
164
|
+
</ToolbarSplit>
|
|
165
|
+
)
|
|
166
|
+
) : (
|
|
167
|
+
menu && (
|
|
168
|
+
splitOnly ? (
|
|
169
|
+
<ToolbarSplit
|
|
170
|
+
open={opened}
|
|
171
|
+
{...splitHandlers}
|
|
172
|
+
>
|
|
173
|
+
<ChevronDownIcon style={styles?.splitIcon} />
|
|
174
|
+
</ToolbarSplit>
|
|
175
|
+
) : (
|
|
176
|
+
<ToolbarSplit
|
|
177
|
+
role="button" tabIndex={disabled ? undefined : 0} aria-label={label} aria-disabled={disabled} aria-haspopup="menu"
|
|
178
|
+
open={opened}
|
|
179
|
+
{...splitHandlers}
|
|
180
|
+
>
|
|
181
|
+
<Cog8ToothIcon style={styles?.splitIcon} />
|
|
182
|
+
</ToolbarSplit>
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
)}
|
|
186
|
+
</ToolbarItemContainerElement>
|
|
187
|
+
</Tooltip>
|
|
188
|
+
{opened && form && (
|
|
189
|
+
<Callout target={`#${id}`}>
|
|
190
|
+
<FormContainer onMouseDown={e => e.stopPropagation()}>
|
|
191
|
+
{form}
|
|
192
|
+
</FormContainer>
|
|
193
|
+
</Callout>
|
|
194
|
+
)}
|
|
195
|
+
</>
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const ToolbarItem = memo<{
|
|
200
|
+
item: ToolbarItemProps;
|
|
201
|
+
styles?: ToolbarProps['styles'];
|
|
202
|
+
openedKey: string | null;
|
|
203
|
+
setOpenedKey: (key: string | null) => void;
|
|
204
|
+
renderSlot: (node: ReactNode) => void;
|
|
205
|
+
}>(function ToolbarItem ({ item, styles, openedKey, setOpenedKey, renderSlot }) {
|
|
206
|
+
if (item === ToolbarItemSplitter) {
|
|
207
|
+
return <ToolbarSplitter />;
|
|
208
|
+
}
|
|
209
|
+
if ('checked' in item) {
|
|
210
|
+
return <ToolbarToggleButton item={item} styles={styles} openedKey={openedKey} setOpenedKey={setOpenedKey} renderSlot={renderSlot} />;
|
|
211
|
+
} else if ('options' in item) {
|
|
212
|
+
return <ToolbarSelectButton item={item} styles={styles} openedKey={openedKey} setOpenedKey={setOpenedKey} renderSlot={renderSlot} />;
|
|
213
|
+
}
|
|
214
|
+
return <ToolbarButton item={item} styles={styles} openedKey={openedKey} setOpenedKey={setOpenedKey} renderSlot={renderSlot} />;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
export default ToolbarItem;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React, { memo, useEffect, useRef } from "react";
|
|
2
|
+
import styled from "styled-components";
|
|
3
|
+
import produce from "immer";
|
|
4
|
+
import { IToolbarItem, IToolbarProps, ToolbarItemContainer } from "./toolbar-item";
|
|
5
|
+
import { ToolbarContainer, useHandlers, ToolbarItemContainerElement } from "./components";
|
|
6
|
+
import Callout from "../callout";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const OptionGroup = styled(ToolbarContainer)`
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
width: max-content;
|
|
12
|
+
height: max-content;
|
|
13
|
+
--aside: 8px;
|
|
14
|
+
--background-color: #f7f7f7;
|
|
15
|
+
--background-color-hover: #fefefe;
|
|
16
|
+
--color: #777;
|
|
17
|
+
--color-hover: #555;
|
|
18
|
+
--blue: #282958;
|
|
19
|
+
background-color: var(--background-color);
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const Option = styled(ToolbarItemContainerElement)`
|
|
23
|
+
width: unset;
|
|
24
|
+
height: var(--height);
|
|
25
|
+
position: relative;
|
|
26
|
+
font-size: 95%;
|
|
27
|
+
padding-left: var(--aside);
|
|
28
|
+
padding-right: 1em;
|
|
29
|
+
align-items: center;
|
|
30
|
+
&[aria-selected="true"] {
|
|
31
|
+
::before {
|
|
32
|
+
display: block;
|
|
33
|
+
position: absolute;
|
|
34
|
+
content: "";
|
|
35
|
+
left: calc(var(--aside) / 2);
|
|
36
|
+
width: calc(var(--aside) / 2);
|
|
37
|
+
top: calc(var(--height) / 8);
|
|
38
|
+
bottom: calc(var(--height) / 8);
|
|
39
|
+
background-color: var(--blue);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
> label {
|
|
43
|
+
user-select: none;
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
}
|
|
46
|
+
:hover, &[aria-selected="true"] {
|
|
47
|
+
color: var(--color-hover);
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const TriggerFlag = styled.span`
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
position: absolute;
|
|
54
|
+
bottom: 0;
|
|
55
|
+
left: 50%;
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
export interface ToolbarSelectButtonItem<T extends string = string> extends IToolbarItem {
|
|
59
|
+
options: {
|
|
60
|
+
key: T;
|
|
61
|
+
icon: (props: React.SVGProps<SVGSVGElement> & {
|
|
62
|
+
title?: string | undefined;
|
|
63
|
+
titleId?: string | undefined;
|
|
64
|
+
}) => JSX.Element;
|
|
65
|
+
label: string;
|
|
66
|
+
/** @default false */
|
|
67
|
+
disabled?: boolean;
|
|
68
|
+
}[];
|
|
69
|
+
value: T;
|
|
70
|
+
onSelect: (value: T) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ToolbarSelectButton = memo<IToolbarProps<ToolbarSelectButtonItem>>(function ToolbarSelectButton(props) {
|
|
74
|
+
const { item, styles, openedKey, setOpenedKey } = props;
|
|
75
|
+
const { key, icon: Icon, disabled, options, value, onSelect } = item;
|
|
76
|
+
const id = `${key}::button`;
|
|
77
|
+
|
|
78
|
+
const opened = openedKey === id;
|
|
79
|
+
const handlers = useHandlers(() => {
|
|
80
|
+
setOpenedKey(opened ? null : id);
|
|
81
|
+
}, disabled ?? false);
|
|
82
|
+
|
|
83
|
+
const openedRef = useRef(opened);
|
|
84
|
+
openedRef.current = opened;
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (opened) {
|
|
88
|
+
const close = (e?: unknown) => {
|
|
89
|
+
if (!openedRef.current) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!e) {
|
|
93
|
+
setOpenedKey(null);
|
|
94
|
+
} else if (e instanceof KeyboardEvent && e.key === 'Escape') {
|
|
95
|
+
setOpenedKey(null);
|
|
96
|
+
} else if (e instanceof MouseEvent) {
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
if (openedRef.current) {
|
|
99
|
+
setOpenedKey(null);
|
|
100
|
+
}
|
|
101
|
+
}, 100);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
document.addEventListener('mousedown', close);
|
|
106
|
+
document.addEventListener('keydown', close);
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
document.removeEventListener('mousedown', close);
|
|
110
|
+
document.removeEventListener('keydown', close);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}, [setOpenedKey, opened]);
|
|
114
|
+
|
|
115
|
+
const currentOption = options.find(opt => opt.key === value);
|
|
116
|
+
const CurrentIcon = currentOption?.icon;
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
<ToolbarItemContainer
|
|
121
|
+
props={produce(props, draft => {
|
|
122
|
+
if (currentOption) {
|
|
123
|
+
draft.item.label = `${draft.item.label}: ${currentOption.label}`;
|
|
124
|
+
}
|
|
125
|
+
})}
|
|
126
|
+
handlers={handlers}
|
|
127
|
+
aria-haspopup="listbox"
|
|
128
|
+
>
|
|
129
|
+
<Icon style={styles?.icon} />
|
|
130
|
+
{CurrentIcon && (
|
|
131
|
+
<CurrentIcon
|
|
132
|
+
style={{
|
|
133
|
+
...styles?.icon,
|
|
134
|
+
position: 'absolute',
|
|
135
|
+
left: 'calc(var(--height) - var(--icon-size) * 1.2)',
|
|
136
|
+
bottom: 'calc((var(--height) - var(--icon-size)) * 0.1)',
|
|
137
|
+
width: 'calc(var(--icon-size) * 0.6)',
|
|
138
|
+
height: 'calc(var(--icon-size) * 0.6)',
|
|
139
|
+
margin: 'calc((var(--height) - var(--icon-size)) * 0.2)',
|
|
140
|
+
filter: 'drop-shadow(0 0 0.5px var(--background-color)) '.repeat(4),
|
|
141
|
+
pointerEvents: 'none',
|
|
142
|
+
color: '#1d1e38',
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
<TriggerFlag aria-hidden id={id} />
|
|
147
|
+
</ToolbarItemContainer>
|
|
148
|
+
{opened && (
|
|
149
|
+
<Callout target={`#${id}`}>
|
|
150
|
+
<OptionGroup role="listbox" aria-activedescendant={`${id}::${value}`} aria-describedby={id} aria-disabled={disabled} onMouseDown={e => e.stopPropagation()}>
|
|
151
|
+
{options.map((option, idx, arr) => {
|
|
152
|
+
const selected = option.key === value;
|
|
153
|
+
const OptionIcon = option.icon;
|
|
154
|
+
const optionId = `${id}::${value}`;
|
|
155
|
+
const prev = arr[(idx + arr.length - 1) % arr.length];
|
|
156
|
+
const next = arr[(idx + 1) % arr.length];
|
|
157
|
+
return (
|
|
158
|
+
<Option
|
|
159
|
+
key={option.key}
|
|
160
|
+
id={optionId}
|
|
161
|
+
role="option"
|
|
162
|
+
aria-disabled={option.disabled ?? false}
|
|
163
|
+
aria-selected={selected}
|
|
164
|
+
split={false}
|
|
165
|
+
tabIndex={0}
|
|
166
|
+
onClick={() => {
|
|
167
|
+
onSelect(option.key);
|
|
168
|
+
setOpenedKey(null);
|
|
169
|
+
}}
|
|
170
|
+
onKeyDown={e => {
|
|
171
|
+
if (e.key === 'ArrowDown') {
|
|
172
|
+
onSelect(next.key);
|
|
173
|
+
} else if (e.key === 'ArrowUp') {
|
|
174
|
+
onSelect(prev.key);
|
|
175
|
+
}
|
|
176
|
+
}}
|
|
177
|
+
ref={e => {
|
|
178
|
+
if (e && selected) {
|
|
179
|
+
e.focus();
|
|
180
|
+
}
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
<OptionIcon style={styles?.icon} />
|
|
184
|
+
<label className="text-xs">{option.label}</label>
|
|
185
|
+
</Option>
|
|
186
|
+
);
|
|
187
|
+
})}
|
|
188
|
+
</OptionGroup>
|
|
189
|
+
</Callout>
|
|
190
|
+
)}
|
|
191
|
+
</>
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
export default ToolbarSelectButton;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { memo } from "react";
|
|
2
|
+
import styled from "styled-components";
|
|
3
|
+
import { IToolbarItem, IToolbarProps, ToolbarItemContainer } from "./toolbar-item";
|
|
4
|
+
import { useHandlers } from "./components";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const ToggleContainer = styled.div<{ checked: boolean }>`
|
|
8
|
+
flex-grow: 0;
|
|
9
|
+
flex-shrink: 0;
|
|
10
|
+
width: calc(var(--icon-size) + 12px);
|
|
11
|
+
height: calc(var(--icon-size) + 12px);
|
|
12
|
+
margin: calc((var(--height) - var(--icon-size) - 12px) / 2);
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
box-shadow: ${({ checked }) => checked ? 'inset 0px 1px 6px 2px rgba(0, 0, 0, 0.33)' : 'inset 0px 1px 4px 1px rgba(136, 136, 136, 0.16)'};
|
|
18
|
+
border-radius: 4px;
|
|
19
|
+
position: relative;
|
|
20
|
+
background-color: #F7F7F722;
|
|
21
|
+
> svg {
|
|
22
|
+
width: var(--icon-size);
|
|
23
|
+
height: var(--icon-size);
|
|
24
|
+
position: absolute;
|
|
25
|
+
color: ${({ checked }) => checked ? '#EDEFF4' : 'var(--color)'};
|
|
26
|
+
--shadow-color: ${({ checked }) => checked ? '#2956bf66' : '#9ba1ab66' || '#52576366'};
|
|
27
|
+
transition: color 120ms;
|
|
28
|
+
}
|
|
29
|
+
::before {
|
|
30
|
+
display: block;
|
|
31
|
+
content: "";
|
|
32
|
+
position: absolute;
|
|
33
|
+
top: 0;
|
|
34
|
+
left: 0;
|
|
35
|
+
width: 100%;
|
|
36
|
+
height: 100%;
|
|
37
|
+
background-color: var(--blue-dark);
|
|
38
|
+
transform: ${({ checked }) => checked ? 'translate(0)' : 'translateX(-100%)'};
|
|
39
|
+
transition: transform 80ms;
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
export interface ToolbarToggleButtonItem extends IToolbarItem {
|
|
44
|
+
checked: boolean;
|
|
45
|
+
onChange: (checked: boolean) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ToolbarToggleButton = memo<IToolbarProps<ToolbarToggleButtonItem>>(function ToolbarToggleButton(props) {
|
|
49
|
+
const { item, styles } = props;
|
|
50
|
+
const { icon: Icon, label, disabled, checked, onChange } = item;
|
|
51
|
+
const handlers = useHandlers(() => onChange(!checked), disabled ?? false);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
<ToolbarItemContainer
|
|
56
|
+
props={props}
|
|
57
|
+
handlers={handlers}
|
|
58
|
+
role="checkbox"
|
|
59
|
+
aria-checked={checked}
|
|
60
|
+
>
|
|
61
|
+
<ToggleContainer checked={checked}>
|
|
62
|
+
<Icon style={styles?.icon} />
|
|
63
|
+
</ToggleContainer>
|
|
64
|
+
</ToolbarItemContainer>
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
export default ToolbarToggleButton;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React, { memo, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import styled from "styled-components";
|
|
4
|
+
import { ShadowDomContext } from "..";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export interface TooltipProps {
|
|
8
|
+
children: JSX.Element;
|
|
9
|
+
content: string | JSX.Element | JSX.Element[];
|
|
10
|
+
/** @default 250 */
|
|
11
|
+
showDelay?: number;
|
|
12
|
+
/** @default 250 */
|
|
13
|
+
hideDelay?: number;
|
|
14
|
+
/** @default 3_000 */
|
|
15
|
+
autoHide?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const attrName = 'data-tooltip-host-id';
|
|
19
|
+
let flag = 0;
|
|
20
|
+
|
|
21
|
+
const Bubble = styled.div`
|
|
22
|
+
border-radius: 1px;
|
|
23
|
+
transform: translate(-50%, -100%);
|
|
24
|
+
filter: drop-shadow(0 1.6px 1.2px rgba(0, 0, 0, 0.15)) drop-shadow(0 -1px 1px rgba(0, 0, 0, 0.12));
|
|
25
|
+
user-select: none;
|
|
26
|
+
::before {
|
|
27
|
+
content: "";
|
|
28
|
+
display: block;
|
|
29
|
+
position: absolute;
|
|
30
|
+
bottom: 0;
|
|
31
|
+
left: 50%;
|
|
32
|
+
width: 8px;
|
|
33
|
+
height: 8px;
|
|
34
|
+
transform: translate(-50%, 50%) rotate(45deg);
|
|
35
|
+
background-color: #fff;
|
|
36
|
+
border-radius: 1px;
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const Tooltip = memo<TooltipProps>(function Tooltip ({ children, content, autoHide = 3_000, showDelay = 250, hideDelay = 250 }) {
|
|
41
|
+
const hostId = useMemo(() => flag++, []);
|
|
42
|
+
const [pos, setPos] = useState<[number, number]>([0, 0]);
|
|
43
|
+
const [show, setShow] = useState(false);
|
|
44
|
+
const [hover, setHover] = useState(false);
|
|
45
|
+
const shadowDomMeta = useContext(ShadowDomContext);
|
|
46
|
+
const { root } = shadowDomMeta;
|
|
47
|
+
const element = typeof children === 'object' ? { ...children as any } : children;
|
|
48
|
+
if ('props' in element) {
|
|
49
|
+
element.props = {
|
|
50
|
+
...element.props,
|
|
51
|
+
[attrName]: hostId,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const autoHideRef = useRef(autoHide);
|
|
56
|
+
autoHideRef.current = autoHide;
|
|
57
|
+
const showDelayRef = useRef(showDelay);
|
|
58
|
+
showDelayRef.current = showDelay;
|
|
59
|
+
const hideDelayRef = useRef(hideDelay);
|
|
60
|
+
hideDelayRef.current = hideDelay;
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const item = root?.querySelector(`[${attrName}="${hostId}"]`) as HTMLElement | null;
|
|
64
|
+
if (item) {
|
|
65
|
+
let showTimer: NodeJS.Timeout | null = null;
|
|
66
|
+
let hideTimer: NodeJS.Timeout | null = null;
|
|
67
|
+
let autoHideTimer: NodeJS.Timeout | null = null;
|
|
68
|
+
const resetTimers = () => {
|
|
69
|
+
for (const timer of [showTimer, hideTimer, autoHideTimer]) {
|
|
70
|
+
if (timer) {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const handleMouseOver = () => {
|
|
76
|
+
resetTimers();
|
|
77
|
+
showTimer = setTimeout(() => {
|
|
78
|
+
const rect = item.getBoundingClientRect();
|
|
79
|
+
setPos([rect.x + rect.width / 2, rect.y]);
|
|
80
|
+
setShow(true);
|
|
81
|
+
autoHideTimer = setTimeout(() => {
|
|
82
|
+
handleMouseOut();
|
|
83
|
+
}, autoHideRef.current);
|
|
84
|
+
}, showDelayRef.current);
|
|
85
|
+
};
|
|
86
|
+
const handleMouseMove = () => {
|
|
87
|
+
for (const timer of [hideTimer, autoHideTimer]) {
|
|
88
|
+
if (timer) {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
autoHideTimer = setTimeout(() => {
|
|
93
|
+
handleMouseOut();
|
|
94
|
+
}, autoHideRef.current);
|
|
95
|
+
};
|
|
96
|
+
const handleMouseOut = () => {
|
|
97
|
+
resetTimers();
|
|
98
|
+
hideTimer = setTimeout(() => {
|
|
99
|
+
setShow(false);
|
|
100
|
+
}, hideDelayRef.current);
|
|
101
|
+
};
|
|
102
|
+
item.addEventListener('mouseover', handleMouseOver);
|
|
103
|
+
item.addEventListener('mousemove', handleMouseMove);
|
|
104
|
+
item.addEventListener('mouseout', handleMouseOut);
|
|
105
|
+
return () => {
|
|
106
|
+
item.removeEventListener('mouseover', handleMouseOver);
|
|
107
|
+
item.removeEventListener('mousemove', handleMouseMove);
|
|
108
|
+
item.removeEventListener('mouseout', handleMouseOut);
|
|
109
|
+
if (autoHideTimer) {
|
|
110
|
+
clearTimeout(autoHideTimer);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}, [root, hostId]);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
{element}
|
|
119
|
+
{(show || hover) && root && createPortal(
|
|
120
|
+
<Bubble
|
|
121
|
+
className="fixed text-xs p-1 px-3 text-gray-500 bg-white z-50"
|
|
122
|
+
onMouseOver={() => setHover(true)}
|
|
123
|
+
onMouseOut={() => setHover(false)}
|
|
124
|
+
style={{ left: pos[0], top: pos[1] - 4 }}
|
|
125
|
+
>
|
|
126
|
+
{content}
|
|
127
|
+
</Bubble>,
|
|
128
|
+
root
|
|
129
|
+
)}
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
export default Tooltip;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is empty and importing it causes no side effect
|
|
3
|
+
* expect creating an empty <style> tag in the <head>.
|
|
4
|
+
* GraphicWalker starts using encapsuled css styles since v0.2.10
|
|
5
|
+
* so it's no more necessary to explicitly import a styled file
|
|
6
|
+
* from the production directory.
|
|
7
|
+
* This file will be temporarily kept to avoid breaking compiling
|
|
8
|
+
* while trying to import the out-dated style file.
|
|
9
|
+
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Droppable } from "react-beautiful-dnd";
|
|
2
|
+
import { Droppable } from "@kanaries/react-beautiful-dnd";
|
|
3
3
|
import { DRAGGABLE_STATE_KEYS } from './fieldsContext';
|
|
4
4
|
import { AestheticFieldContainer } from './components'
|
|
5
5
|
import OBFieldContainer from './obComponents/obFContainer';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Draggable, DroppableProvided } from 'react-beautiful-dnd';
|
|
2
|
+
import { Draggable, DroppableProvided } from '@kanaries/react-beautiful-dnd';
|
|
3
3
|
import { observer } from 'mobx-react-lite';
|
|
4
4
|
import { useGlobalStore } from '../../store';
|
|
5
5
|
import DataTypeIcon from '../../components/dataTypeIcon';
|
|
@@ -19,7 +19,7 @@ const DimFields: React.FC<Props> = props => {
|
|
|
19
19
|
return (
|
|
20
20
|
<>
|
|
21
21
|
<FieldPill
|
|
22
|
-
className="pt-0.5 pb-0.5 pl-2 pr-2 m-1 text-xs hover:bg-blue-100 rounded-full truncate"
|
|
22
|
+
className="pt-0.5 pb-0.5 pl-2 pr-2 mx-0 m-1 text-xs hover:bg-blue-100 rounded-full truncate border border-transparent"
|
|
23
23
|
ref={provided.innerRef}
|
|
24
24
|
isDragging={snapshot.isDragging}
|
|
25
25
|
{...provided.draggableProps}
|
|
@@ -28,7 +28,7 @@ const DimFields: React.FC<Props> = props => {
|
|
|
28
28
|
<DataTypeIcon dataType={f.semanticType} analyticType={f.analyticType} /> {f.name}
|
|
29
29
|
</FieldPill>
|
|
30
30
|
{
|
|
31
|
-
<FieldPill className={`pt-0.5 pb-0.5 pl-2 pr-2 m-1 text-xs hover:bg-blue-100 rounded-full border-blue-400 border truncate ${snapshot.isDragging ? '' : 'hidden'}`}
|
|
31
|
+
<FieldPill className={`pt-0.5 pb-0.5 pl-2 pr-2 mx-0 m-1 text-xs hover:bg-blue-100 rounded-full border-blue-400 border truncate ${snapshot.isDragging ? '' : 'hidden'}`}
|
|
32
32
|
isDragging={snapshot.isDragging}
|
|
33
33
|
>
|
|
34
34
|
<DataTypeIcon dataType={f.semanticType} analyticType={f.analyticType} /> {f.name}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Droppable } from "react-beautiful-dnd";
|
|
2
|
+
import { Droppable } from "@kanaries/react-beautiful-dnd";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import { NestContainer } from "../../components/container";
|
|
@@ -16,7 +16,7 @@ const DatasetFields: React.FC = (props) => {
|
|
|
16
16
|
const { t } = useTranslation("translation", { keyPrefix: "main.tabpanel.DatasetFields" });
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<DSContainer className="flex md:flex-col" style={{ paddingBlock: 0 }}>
|
|
19
|
+
<DSContainer className="flex md:flex-col" style={{ paddingBlock: 0, paddingInline: '0.6em' }}>
|
|
20
20
|
<h4 className="text-xs mb-2 flex-grow-0 cursor-default select-none mt-2">{t("field_list")}</h4>
|
|
21
21
|
<div className="pd-1 overflow-y-auto" style={{ maxHeight: "380px", minHeight: '100px' }}>
|
|
22
22
|
<Droppable droppableId="dimensions" direction="vertical">
|