Skip to content

Commit d2f67e6

Browse files
ryan-didwelle
andauthored
feat: editable element stats (#6382)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
1 parent 22b3927 commit d2f67e6

40 files changed

+3588
-405
lines changed

excalidraw-app/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
margin-bottom: auto;
2626
margin-inline-start: auto;
2727
margin-inline-end: 0.6em;
28+
z-index: var(--zIndex-layerUI);
2829

2930
svg {
3031
width: 1.2rem;

packages/excalidraw/actions/actionCanvas.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export const actionClearCanvas = register({
104104
exportBackground: appState.exportBackground,
105105
exportEmbedScene: appState.exportEmbedScene,
106106
gridSize: appState.gridSize,
107-
showStats: appState.showStats,
107+
stats: appState.stats,
108108
pasteDialog: appState.pasteDialog,
109109
activeTool:
110110
appState.activeTool.type === "image"

packages/excalidraw/actions/actionToggleStats.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,22 @@ import { StoreAction } from "../store";
55

66
export const actionToggleStats = register({
77
name: "stats",
8-
label: "stats.title",
8+
label: "stats.fullTitle",
99
icon: abacusIcon,
1010
paletteName: "Toggle stats",
1111
viewMode: true,
1212
trackEvent: { category: "menu" },
13+
keywords: ["edit", "attributes", "customize"],
1314
perform(elements, appState) {
1415
return {
1516
appState: {
1617
...appState,
17-
showStats: !this.checked!(appState),
18+
stats: { ...appState.stats, open: !this.checked!(appState) },
1819
},
1920
storeAction: StoreAction.NONE,
2021
};
2122
},
22-
checked: (appState) => appState.showStats,
23+
checked: (appState) => appState.stats.open,
2324
keyTest: (event) =>
2425
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH,
2526
});

packages/excalidraw/actions/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ export type ActionName =
135135
| "createContainerFromText"
136136
| "wrapTextInContainer"
137137
| "commandPalette"
138-
| "autoResize";
138+
| "autoResize"
139+
| "elementStats";
139140

140141
export type PanelComponentProps = {
141142
elements: readonly ExcalidrawElement[];

packages/excalidraw/appState.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DEFAULT_FONT_SIZE,
66
DEFAULT_TEXT_ALIGN,
77
EXPORT_SCALES,
8+
STATS_PANELS,
89
THEME,
910
} from "./constants";
1011
import type { AppState, NormalizedZoomValue } from "./types";
@@ -80,7 +81,10 @@ export const getDefaultAppState = (): Omit<
8081
selectedElementsAreBeingDragged: false,
8182
selectionElement: null,
8283
shouldCacheIgnoreZoom: false,
83-
showStats: false,
84+
stats: {
85+
open: false,
86+
panels: STATS_PANELS.generalStats | STATS_PANELS.elementProperties,
87+
},
8488
startBoundElement: null,
8589
suggestedBindings: [],
8690
frameRendering: { enabled: true, clip: true, name: true, outline: true },
@@ -196,7 +200,7 @@ const APP_STATE_STORAGE_CONF = (<
196200
},
197201
selectionElement: { browser: false, export: false, server: false },
198202
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
199-
showStats: { browser: true, export: false, server: false },
203+
stats: { browser: true, export: false, server: false },
200204
startBoundElement: { browser: false, export: false, server: false },
201205
suggestedBindings: { browser: false, export: false, server: false },
202206
frameRendering: { browser: false, export: false, server: false },

packages/excalidraw/components/App.tsx

Lines changed: 77 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2135,95 +2135,96 @@ class App extends React.Component<AppProps, AppState> {
21352135
});
21362136
};
21372137

2138-
private syncActionResult = withBatchedUpdates(
2139-
(actionResult: ActionResult) => {
2140-
if (this.unmounted || actionResult === false) {
2141-
return;
2142-
}
2138+
public syncActionResult = withBatchedUpdates((actionResult: ActionResult) => {
2139+
if (this.unmounted || actionResult === false) {
2140+
return;
2141+
}
21432142

2144-
let editingElement: AppState["editingElement"] | null = null;
2145-
if (actionResult.elements) {
2146-
actionResult.elements.forEach((element) => {
2147-
if (
2148-
this.state.editingElement?.id === element.id &&
2149-
this.state.editingElement !== element &&
2150-
isNonDeletedElement(element)
2151-
) {
2152-
editingElement = element;
2153-
}
2154-
});
2143+
if (actionResult.storeAction === StoreAction.UPDATE) {
2144+
this.store.shouldUpdateSnapshot();
2145+
} else if (actionResult.storeAction === StoreAction.CAPTURE) {
2146+
this.store.shouldCaptureIncrement();
2147+
}
21552148

2156-
if (actionResult.storeAction === StoreAction.UPDATE) {
2157-
this.store.shouldUpdateSnapshot();
2158-
} else if (actionResult.storeAction === StoreAction.CAPTURE) {
2159-
this.store.shouldCaptureIncrement();
2149+
let didUpdate = false;
2150+
2151+
let editingElement: AppState["editingElement"] | null = null;
2152+
if (actionResult.elements) {
2153+
actionResult.elements.forEach((element) => {
2154+
if (
2155+
this.state.editingElement?.id === element.id &&
2156+
this.state.editingElement !== element &&
2157+
isNonDeletedElement(element)
2158+
) {
2159+
editingElement = element;
21602160
}
2161+
});
21612162

2162-
this.scene.replaceAllElements(actionResult.elements);
2163-
}
2163+
this.scene.replaceAllElements(actionResult.elements);
2164+
didUpdate = true;
2165+
}
21642166

2165-
if (actionResult.files) {
2166-
this.files = actionResult.replaceFiles
2167-
? actionResult.files
2168-
: { ...this.files, ...actionResult.files };
2169-
this.addNewImagesToImageCache();
2167+
if (actionResult.files) {
2168+
this.files = actionResult.replaceFiles
2169+
? actionResult.files
2170+
: { ...this.files, ...actionResult.files };
2171+
this.addNewImagesToImageCache();
2172+
}
2173+
2174+
if (actionResult.appState || editingElement || this.state.contextMenu) {
2175+
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
2176+
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
2177+
let gridSize = actionResult?.appState?.gridSize || null;
2178+
const theme =
2179+
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
2180+
const name = actionResult?.appState?.name ?? this.state.name;
2181+
const errorMessage =
2182+
actionResult?.appState?.errorMessage ?? this.state.errorMessage;
2183+
if (typeof this.props.viewModeEnabled !== "undefined") {
2184+
viewModeEnabled = this.props.viewModeEnabled;
21702185
}
21712186

2172-
if (actionResult.appState || editingElement || this.state.contextMenu) {
2173-
if (actionResult.storeAction === StoreAction.UPDATE) {
2174-
this.store.shouldUpdateSnapshot();
2175-
} else if (actionResult.storeAction === StoreAction.CAPTURE) {
2176-
this.store.shouldCaptureIncrement();
2177-
}
2187+
if (typeof this.props.zenModeEnabled !== "undefined") {
2188+
zenModeEnabled = this.props.zenModeEnabled;
2189+
}
21782190

2179-
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
2180-
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
2181-
let gridSize = actionResult?.appState?.gridSize || null;
2182-
const theme =
2183-
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
2184-
const name = actionResult?.appState?.name ?? this.state.name;
2185-
const errorMessage =
2186-
actionResult?.appState?.errorMessage ?? this.state.errorMessage;
2187-
if (typeof this.props.viewModeEnabled !== "undefined") {
2188-
viewModeEnabled = this.props.viewModeEnabled;
2189-
}
2191+
if (typeof this.props.gridModeEnabled !== "undefined") {
2192+
gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
2193+
}
21902194

2191-
if (typeof this.props.zenModeEnabled !== "undefined") {
2192-
zenModeEnabled = this.props.zenModeEnabled;
2193-
}
2195+
editingElement =
2196+
editingElement || actionResult.appState?.editingElement || null;
21942197

2195-
if (typeof this.props.gridModeEnabled !== "undefined") {
2196-
gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
2197-
}
2198+
if (editingElement?.isDeleted) {
2199+
editingElement = null;
2200+
}
21982201

2199-
editingElement =
2200-
editingElement || actionResult.appState?.editingElement || null;
2202+
this.setState((state) => {
2203+
// using Object.assign instead of spread to fool TS 4.2.2+ into
2204+
// regarding the resulting type as not containing undefined
2205+
// (which the following expression will never contain)
2206+
return Object.assign(actionResult.appState || {}, {
2207+
// NOTE this will prevent opening context menu using an action
2208+
// or programmatically from the host, so it will need to be
2209+
// rewritten later
2210+
contextMenu: null,
2211+
editingElement,
2212+
viewModeEnabled,
2213+
zenModeEnabled,
2214+
gridSize,
2215+
theme,
2216+
name,
2217+
errorMessage,
2218+
});
2219+
});
22012220

2202-
if (editingElement?.isDeleted) {
2203-
editingElement = null;
2204-
}
2221+
didUpdate = true;
2222+
}
22052223

2206-
this.setState((state) => {
2207-
// using Object.assign instead of spread to fool TS 4.2.2+ into
2208-
// regarding the resulting type as not containing undefined
2209-
// (which the following expression will never contain)
2210-
return Object.assign(actionResult.appState || {}, {
2211-
// NOTE this will prevent opening context menu using an action
2212-
// or programmatically from the host, so it will need to be
2213-
// rewritten later
2214-
contextMenu: null,
2215-
editingElement,
2216-
viewModeEnabled,
2217-
zenModeEnabled,
2218-
gridSize,
2219-
theme,
2220-
name,
2221-
errorMessage,
2222-
});
2223-
});
2224-
}
2225-
},
2226-
);
2224+
if (!didUpdate && actionResult.storeAction !== StoreAction.NONE) {
2225+
this.scene.triggerUpdate();
2226+
}
2227+
});
22272228

22282229
// Lifecycle
22292230

packages/excalidraw/components/HelpDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
285285
shortcuts={[getShortcutKey("Alt+Shift+D")]}
286286
/>
287287
<Shortcut
288-
label={t("stats.title")}
288+
label={t("stats.fullTitle")}
289289
shortcuts={[getShortcutKey("Alt+/")]}
290290
/>
291291
<Shortcut

packages/excalidraw/components/LayerUI.scss

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,99 @@
2727
& > * {
2828
pointer-events: var(--ui-pointerEvents);
2929
}
30+
31+
& > .Stats {
32+
width: 204px;
33+
position: absolute;
34+
top: 60px;
35+
font-size: 12px;
36+
z-index: var(--zIndex-layerUI);
37+
pointer-events: var(--ui-pointerEvents);
38+
39+
.title {
40+
display: flex;
41+
justify-content: space-between;
42+
align-items: center;
43+
margin-bottom: 12px;
44+
45+
h2 {
46+
margin: 0;
47+
}
48+
}
49+
50+
.sectionContent {
51+
display: flex;
52+
flex-direction: column;
53+
align-items: center;
54+
justify-content: center;
55+
}
56+
57+
.elementType {
58+
font-size: 12px;
59+
font-weight: 700;
60+
margin-top: 8px;
61+
}
62+
63+
.elementsCount {
64+
width: 100%;
65+
font-size: 12px;
66+
display: flex;
67+
justify-content: space-between;
68+
margin-top: 8px;
69+
}
70+
71+
.statsItem {
72+
margin-top: 8px;
73+
width: 100%;
74+
margin-bottom: 4px;
75+
display: grid;
76+
gap: 4px;
77+
78+
.label {
79+
margin-right: 4px;
80+
}
81+
}
82+
83+
h3 {
84+
white-space: nowrap;
85+
margin: 0;
86+
}
87+
88+
.close {
89+
height: 16px;
90+
width: 16px;
91+
cursor: pointer;
92+
svg {
93+
width: 100%;
94+
height: 100%;
95+
}
96+
}
97+
98+
table {
99+
width: 100%;
100+
th {
101+
border-bottom: 1px solid var(--input-border-color);
102+
padding: 4px;
103+
}
104+
tr {
105+
td:nth-child(2) {
106+
min-width: 24px;
107+
text-align: right;
108+
}
109+
}
110+
}
111+
112+
.divider {
113+
width: 100%;
114+
height: 1px;
115+
background-color: var(--default-border-color);
116+
}
117+
118+
:root[dir="rtl"] & {
119+
left: 12px;
120+
right: initial;
121+
}
122+
}
30123
}
31124

32125
&__footer {

0 commit comments

Comments
 (0)