Merge branch 'master' into mtolmacs/feat/fixed-point-simple-arrow-binding

This commit is contained in:
Mark Tolmacs
2025-10-10 22:47:40 +02:00
50 changed files with 2428 additions and 715 deletions

View File

@@ -1,5 +1,3 @@
version: "3.8"
services: services:
excalidraw: excalidraw:
build: build:

View File

@@ -545,3 +545,8 @@ export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20;
export const DOUBLE_TAP_POSITION_THRESHOLD = 35; export const DOUBLE_TAP_POSITION_THRESHOLD = 35;
export const BIND_MODE_TIMEOUT = 700; // ms export const BIND_MODE_TIMEOUT = 700; // ms
// glass background for mobile action buttons
export const MOBILE_ACTION_BUTTON_BG = {
background: "var(--mobile-action-button-bg)",
} as const;

View File

@@ -10,7 +10,13 @@ export const hasBackground = (type: ElementOrToolType) =>
type === "freedraw"; type === "freedraw";
export const hasStrokeColor = (type: ElementOrToolType) => export const hasStrokeColor = (type: ElementOrToolType) =>
type !== "image" && type !== "frame" && type !== "magicframe"; type === "rectangle" ||
type === "ellipse" ||
type === "diamond" ||
type === "freedraw" ||
type === "arrow" ||
type === "line" ||
type === "text";
export const hasStrokeWidth = (type: ElementOrToolType) => export const hasStrokeWidth = (type: ElementOrToolType) =>
type === "rectangle" || type === "rectangle" ||

View File

@@ -122,7 +122,10 @@ export const actionClearCanvas = register({
pasteDialog: appState.pasteDialog, pasteDialog: appState.pasteDialog,
activeTool: activeTool:
appState.activeTool.type === "image" appState.activeTool.type === "image"
? { ...appState.activeTool, type: app.defaultSelectionTool } ? {
...appState.activeTool,
type: app.state.preferredSelectionTool.type,
}
: appState.activeTool, : appState.activeTool,
}, },
captureUpdate: CaptureUpdateAction.IMMEDIATELY, captureUpdate: CaptureUpdateAction.IMMEDIATELY,
@@ -502,7 +505,7 @@ export const actionToggleEraserTool = register({
if (isEraserActive(appState)) { if (isEraserActive(appState)) {
activeTool = updateActiveTool(appState, { activeTool = updateActiveTool(appState, {
...(appState.activeTool.lastActiveTool || { ...(appState.activeTool.lastActiveTool || {
type: app.defaultSelectionTool, type: app.state.preferredSelectionTool.type,
}), }),
lastActiveToolBeforeEraser: null, lastActiveToolBeforeEraser: null,
}); });
@@ -533,7 +536,7 @@ export const actionToggleLassoTool = register({
icon: LassoIcon, icon: LassoIcon,
trackEvent: { category: "toolbar" }, trackEvent: { category: "toolbar" },
predicate: (elements, appState, props, app) => { predicate: (elements, appState, props, app) => {
return app.defaultSelectionTool !== "lasso"; return app.state.preferredSelectionTool.type !== "lasso";
}, },
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
let activeTool: AppState["activeTool"]; let activeTool: AppState["activeTool"];

View File

@@ -1,4 +1,8 @@
import { KEYS, updateActiveTool } from "@excalidraw/common"; import {
KEYS,
MOBILE_ACTION_BUTTON_BG,
updateActiveTool,
} from "@excalidraw/common";
import { getNonDeletedElements } from "@excalidraw/element"; import { getNonDeletedElements } from "@excalidraw/element";
import { fixBindingsAfterDeletion } from "@excalidraw/element"; import { fixBindingsAfterDeletion } from "@excalidraw/element";
@@ -281,7 +285,7 @@ export const actionDeleteSelected = register({
appState: { appState: {
...nextAppState, ...nextAppState,
activeTool: updateActiveTool(appState, { activeTool: updateActiveTool(appState, {
type: app.defaultSelectionTool, type: app.state.preferredSelectionTool.type,
}), }),
multiElement: null, multiElement: null,
newElement: null, newElement: null,
@@ -306,7 +310,15 @@ export const actionDeleteSelected = register({
title={t("labels.delete")} title={t("labels.delete")}
aria-label={t("labels.delete")} aria-label={t("labels.delete")}
onClick={() => updateData(null)} onClick={() => updateData(null)}
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)} disabled={
!isSomeElementSelected(getNonDeletedElements(elements), appState)
}
style={{
...(appState.stylesPanelMode === "mobile" &&
appState.openPopup !== "compactOtherProperties"
? MOBILE_ACTION_BUTTON_BG
: {}),
}}
/> />
), ),
}); });

View File

@@ -1,6 +1,7 @@
import { import {
DEFAULT_GRID_SIZE, DEFAULT_GRID_SIZE,
KEYS, KEYS,
MOBILE_ACTION_BUTTON_BG,
arrayToMap, arrayToMap,
getShortcutKey, getShortcutKey,
} from "@excalidraw/common"; } from "@excalidraw/common";
@@ -115,7 +116,15 @@ export const actionDuplicateSelection = register({
)}`} )}`}
aria-label={t("labels.duplicateSelection")} aria-label={t("labels.duplicateSelection")}
onClick={() => updateData(null)} onClick={() => updateData(null)}
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)} disabled={
!isSomeElementSelected(getNonDeletedElements(elements), appState)
}
style={{
...(appState.stylesPanelMode === "mobile" &&
appState.openPopup !== "compactOtherProperties"
? MOBILE_ACTION_BUTTON_BG
: {}),
}}
/> />
), ),
}); });

View File

@@ -267,13 +267,13 @@ export const actionFinalize = register<FormData>({
if (appState.activeTool.type === "eraser") { if (appState.activeTool.type === "eraser") {
activeTool = updateActiveTool(appState, { activeTool = updateActiveTool(appState, {
...(appState.activeTool.lastActiveTool || { ...(appState.activeTool.lastActiveTool || {
type: app.defaultSelectionTool, type: app.state.preferredSelectionTool.type,
}), }),
lastActiveToolBeforeEraser: null, lastActiveToolBeforeEraser: null,
}); });
} else { } else {
activeTool = updateActiveTool(appState, { activeTool = updateActiveTool(appState, {
type: app.defaultSelectionTool, type: app.state.preferredSelectionTool.type,
}); });
} }

View File

@@ -1,4 +1,10 @@
import { isWindows, KEYS, matchKey, arrayToMap } from "@excalidraw/common"; import {
isWindows,
KEYS,
matchKey,
arrayToMap,
MOBILE_ACTION_BUTTON_BG,
} from "@excalidraw/common";
import { CaptureUpdateAction } from "@excalidraw/element"; import { CaptureUpdateAction } from "@excalidraw/element";
@@ -67,7 +73,7 @@ export const createUndoAction: ActionCreator = (history) => ({
), ),
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && matchKey(event, KEYS.Z) && !event.shiftKey, event[KEYS.CTRL_OR_CMD] && matchKey(event, KEYS.Z) && !event.shiftKey,
PanelComponent: ({ updateData, data }) => { PanelComponent: ({ appState, updateData, data }) => {
const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>( const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
history.onHistoryChangedEmitter, history.onHistoryChangedEmitter,
new HistoryChangedEvent( new HistoryChangedEvent(
@@ -85,6 +91,11 @@ export const createUndoAction: ActionCreator = (history) => ({
size={data?.size || "medium"} size={data?.size || "medium"}
disabled={isUndoStackEmpty} disabled={isUndoStackEmpty}
data-testid="button-undo" data-testid="button-undo"
style={{
...(appState.stylesPanelMode === "mobile"
? MOBILE_ACTION_BUTTON_BG
: {}),
}}
/> />
); );
}, },
@@ -103,7 +114,7 @@ export const createRedoAction: ActionCreator = (history) => ({
keyTest: (event) => keyTest: (event) =>
(event[KEYS.CTRL_OR_CMD] && event.shiftKey && matchKey(event, KEYS.Z)) || (event[KEYS.CTRL_OR_CMD] && event.shiftKey && matchKey(event, KEYS.Z)) ||
(isWindows && event.ctrlKey && !event.shiftKey && matchKey(event, KEYS.Y)), (isWindows && event.ctrlKey && !event.shiftKey && matchKey(event, KEYS.Y)),
PanelComponent: ({ updateData, data }) => { PanelComponent: ({ appState, updateData, data }) => {
const { isRedoStackEmpty } = useEmitter( const { isRedoStackEmpty } = useEmitter(
history.onHistoryChangedEmitter, history.onHistoryChangedEmitter,
new HistoryChangedEvent( new HistoryChangedEvent(
@@ -121,6 +132,11 @@ export const createRedoAction: ActionCreator = (history) => ({
size={data?.size || "medium"} size={data?.size || "medium"}
disabled={isRedoStackEmpty} disabled={isRedoStackEmpty}
data-testid="button-redo" data-testid="button-redo"
style={{
...(appState.stylesPanelMode === "mobile"
? MOBILE_ACTION_BUTTON_BG
: {}),
}}
/> />
); );
}, },

View File

@@ -352,7 +352,10 @@ export const actionChangeStrokeColor = register<
elements={elements} elements={elements}
appState={appState} appState={appState}
updateData={updateData} updateData={updateData}
compactMode={appState.stylesPanelMode === "compact"} compactMode={
appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile"
}
/> />
</> </>
), ),
@@ -434,7 +437,10 @@ export const actionChangeBackgroundColor = register<
elements={elements} elements={elements}
appState={appState} appState={appState}
updateData={updateData} updateData={updateData}
compactMode={appState.stylesPanelMode === "compact"} compactMode={
appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile"
}
/> />
</> </>
), ),
@@ -539,9 +545,7 @@ export const actionChangeStrokeWidth = register<
}, },
PanelComponent: ({ elements, appState, updateData, app, data }) => ( PanelComponent: ({ elements, appState, updateData, app, data }) => (
<fieldset> <fieldset>
{appState.stylesPanelMode === "full" && ( <legend>{t("labels.strokeWidth")}</legend>
<legend>{t("labels.strokeWidth")}</legend>
)}
<div className="buttonList"> <div className="buttonList">
<RadioSelection <RadioSelection
group="stroke-width" group="stroke-width"
@@ -598,9 +602,7 @@ export const actionChangeSloppiness = register<ExcalidrawElement["roughness"]>({
}, },
PanelComponent: ({ elements, appState, updateData, app, data }) => ( PanelComponent: ({ elements, appState, updateData, app, data }) => (
<fieldset> <fieldset>
{appState.stylesPanelMode === "full" && ( <legend>{t("labels.sloppiness")}</legend>
<legend>{t("labels.sloppiness")}</legend>
)}
<div className="buttonList"> <div className="buttonList">
<RadioSelection <RadioSelection
group="sloppiness" group="sloppiness"
@@ -655,9 +657,7 @@ export const actionChangeStrokeStyle = register<
}, },
PanelComponent: ({ elements, appState, updateData, app, data }) => ( PanelComponent: ({ elements, appState, updateData, app, data }) => (
<fieldset> <fieldset>
{appState.stylesPanelMode === "full" && ( <legend>{t("labels.strokeStyle")}</legend>
<legend>{t("labels.strokeStyle")}</legend>
)}
<div className="buttonList"> <div className="buttonList">
<RadioSelection <RadioSelection
group="strokeStyle" group="strokeStyle"
@@ -734,7 +734,7 @@ export const actionChangeFontSize = register<ExcalidrawTextElement["fontSize"]>(
value, value,
); );
}, },
PanelComponent: ({ elements, appState, updateData, app }) => ( PanelComponent: ({ elements, appState, updateData, app, data }) => (
<fieldset> <fieldset>
<legend>{t("labels.fontSize")}</legend> <legend>{t("labels.fontSize")}</legend>
<div className="buttonList"> <div className="buttonList">
@@ -793,7 +793,15 @@ export const actionChangeFontSize = register<ExcalidrawTextElement["fontSize"]>(
? null ? null
: appState.currentItemFontSize || DEFAULT_FONT_SIZE, : appState.currentItemFontSize || DEFAULT_FONT_SIZE,
)} )}
onChange={(value) => updateData(value)} onChange={(value) => {
withCaretPositionPreservation(
() => updateData(value),
appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile",
!!appState.editingTextElement,
data?.onPreventClose,
);
}}
/> />
</div> </div>
</fieldset> </fieldset>
@@ -1059,7 +1067,7 @@ export const actionChangeFontFamily = register<{
return result; return result;
}, },
PanelComponent: ({ elements, appState, app, updateData, data }) => { PanelComponent: ({ elements, appState, app, updateData }) => {
const cachedElementsRef = useRef<ElementsMap>(new Map()); const cachedElementsRef = useRef<ElementsMap>(new Map());
const prevSelectedFontFamilyRef = useRef<number | null>(null); const prevSelectedFontFamilyRef = useRef<number | null>(null);
// relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them // relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
@@ -1136,7 +1144,7 @@ export const actionChangeFontFamily = register<{
}, []); }, []);
return ( return (
<fieldset> <>
{appState.stylesPanelMode === "full" && ( {appState.stylesPanelMode === "full" && (
<legend>{t("labels.fontFamily")}</legend> <legend>{t("labels.fontFamily")}</legend>
)} )}
@@ -1144,7 +1152,7 @@ export const actionChangeFontFamily = register<{
isOpened={appState.openPopup === "fontFamily"} isOpened={appState.openPopup === "fontFamily"}
selectedFontFamily={selectedFontFamily} selectedFontFamily={selectedFontFamily}
hoveredFontFamily={appState.currentHoveredFontFamily} hoveredFontFamily={appState.currentHoveredFontFamily}
compactMode={appState.stylesPanelMode === "compact"} compactMode={appState.stylesPanelMode !== "full"}
onSelect={(fontFamily) => { onSelect={(fontFamily) => {
withCaretPositionPreservation( withCaretPositionPreservation(
() => { () => {
@@ -1156,7 +1164,8 @@ export const actionChangeFontFamily = register<{
// defensive clear so immediate close won't abuse the cached elements // defensive clear so immediate close won't abuse the cached elements
cachedElementsRef.current.clear(); cachedElementsRef.current.clear();
}, },
appState.stylesPanelMode === "compact", appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile",
!!appState.editingTextElement, !!appState.editingTextElement,
); );
}} }}
@@ -1232,7 +1241,8 @@ export const actionChangeFontFamily = register<{
// Refocus text editor when font picker closes if we were editing text // Refocus text editor when font picker closes if we were editing text
if ( if (
appState.stylesPanelMode === "compact" && (appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile") &&
appState.editingTextElement appState.editingTextElement
) { ) {
restoreCaretPosition(null); // Just refocus without saved position restoreCaretPosition(null); // Just refocus without saved position
@@ -1240,7 +1250,7 @@ export const actionChangeFontFamily = register<{
} }
}} }}
/> />
</fieldset> </>
); );
}, },
}); });
@@ -1333,7 +1343,8 @@ export const actionChangeTextAlign = register<TextAlign>({
onChange={(value) => { onChange={(value) => {
withCaretPositionPreservation( withCaretPositionPreservation(
() => updateData(value), () => updateData(value),
appState.stylesPanelMode === "compact", appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile",
!!appState.editingTextElement, !!appState.editingTextElement,
data?.onPreventClose, data?.onPreventClose,
); );
@@ -1432,7 +1443,8 @@ export const actionChangeVerticalAlign = register<VerticalAlign>({
onChange={(value) => { onChange={(value) => {
withCaretPositionPreservation( withCaretPositionPreservation(
() => updateData(value), () => updateData(value),
appState.stylesPanelMode === "compact", appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile",
!!appState.editingTextElement, !!appState.editingTextElement,
data?.onPreventClose, data?.onPreventClose,
); );
@@ -1698,8 +1710,8 @@ export const actionChangeArrowProperties = register({
PanelComponent: ({ elements, appState, updateData, app, renderAction }) => { PanelComponent: ({ elements, appState, updateData, app, renderAction }) => {
return ( return (
<div className="selected-shape-actions"> <div className="selected-shape-actions">
{renderAction("changeArrowType")}
{renderAction("changeArrowhead")} {renderAction("changeArrowhead")}
{renderAction("changeArrowType")}
</div> </div>
); );
}, },

View File

@@ -55,6 +55,10 @@ export const getDefaultAppState = (): Omit<
fromSelection: false, fromSelection: false,
lastActiveTool: null, lastActiveTool: null,
}, },
preferredSelectionTool: {
type: "selection",
initialized: false,
},
penMode: false, penMode: false,
penDetected: false, penDetected: false,
errorMessage: null, errorMessage: null,
@@ -177,6 +181,7 @@ const APP_STATE_STORAGE_CONF = (<
editingTextElement: { browser: false, export: false, server: false }, editingTextElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false }, editingGroupId: { browser: true, export: false, server: false },
activeTool: { browser: true, export: false, server: false }, activeTool: { browser: true, export: false, server: false },
preferredSelectionTool: { browser: true, export: false, server: false },
penMode: { browser: true, export: false, server: false }, penMode: { browser: true, export: false, server: false },
penDetected: { browser: true, export: false, server: false }, penDetected: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false }, errorMessage: { browser: false, export: false, server: false },
@@ -250,7 +255,7 @@ const APP_STATE_STORAGE_CONF = (<
lockedMultiSelections: { browser: true, export: true, server: true }, lockedMultiSelections: { browser: true, export: true, server: true },
activeLockedId: { browser: false, export: false, server: false }, activeLockedId: { browser: false, export: false, server: false },
bindMode: { browser: true, export: false, server: false }, bindMode: { browser: true, export: false, server: false },
stylesPanelMode: { browser: true, export: false, server: false }, stylesPanelMode: { browser: false, export: false, server: false },
}); });
const _clearAppStateForStorage = < const _clearAppStateForStorage = <

View File

@@ -106,15 +106,15 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 2.5rem; min-height: 2.5rem;
pointer-events: auto;
--default-button-size: 2rem; --default-button-size: 2rem;
.compact-action-button { .compact-action-button {
width: 2rem; width: var(--mobile-action-button-size);
height: 2rem; height: var(--mobile-action-button-size);
border: none; border: none;
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
background: transparent;
color: var(--color-on-surface); color: var(--color-on-surface);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@@ -122,24 +122,20 @@
justify-content: center; justify-content: center;
transition: all 0.2s ease; transition: all 0.2s ease;
background: var(--mobile-action-button-bg);
svg { svg {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
flex: 0 0 auto; flex: 0 0 auto;
} }
&:hover { &.active {
background: var(--button-hover-bg, var(--island-bg-color)); background: var(
border-color: var( --color-surface-primary-container,
--button-hover-border, var(--mobile-action-button-bg)
var(--button-border, var(--default-border-color))
); );
} }
&:active {
background: var(--button-active-bg, var(--island-bg-color));
border-color: var(--button-active-border, var(--color-primary-darkest));
}
} }
.compact-popover-content { .compact-popover-content {
@@ -167,6 +163,19 @@
} }
} }
} }
.ToolIcon {
.ToolIcon__icon {
width: var(--mobile-action-button-size);
height: var(--mobile-action-button-size);
background: var(--mobile-action-button-bg);
&:hover {
background-color: transparent;
}
}
}
} }
.compact-shape-actions-island { .compact-shape-actions-island {
@@ -174,29 +183,18 @@
overflow-x: hidden; overflow-x: hidden;
} }
.compact-popover-content { .mobile-shape-actions {
.popover-section { z-index: 999;
margin-bottom: 1rem; display: flex;
flex-direction: row;
&:last-child { justify-content: space-between;
margin-bottom: 0; width: 100%;
} background: transparent;
border-radius: var(--border-radius-lg);
.popover-section-title { box-shadow: none;
font-size: 0.75rem; overflow: none;
font-weight: 600; scrollbar-width: none;
color: var(--color-text-secondary); -ms-overflow-style: none;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.buttonList {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
}
} }
.shape-actions-theme-scope { .shape-actions-theme-scope {

File diff suppressed because it is too large Load Diff

View File

@@ -672,14 +672,9 @@ class App extends React.Component<AppProps, AppState> {
>(); >();
onRemoveEventListenersEmitter = new Emitter<[]>(); onRemoveEventListenersEmitter = new Emitter<[]>();
defaultSelectionTool: "selection" | "lasso" = "selection";
constructor(props: AppProps) { constructor(props: AppProps) {
super(props); super(props);
const defaultAppState = getDefaultAppState(); const defaultAppState = getDefaultAppState();
this.defaultSelectionTool = isMobileOrTablet()
? ("lasso" as const)
: ("selection" as const);
const { const {
excalidrawAPI, excalidrawAPI,
viewModeEnabled = false, viewModeEnabled = false,
@@ -1871,7 +1866,7 @@ class App extends React.Component<AppProps, AppState> {
public render() { public render() {
const selectedElements = this.scene.getSelectedElements(this.state); const selectedElements = this.scene.getSelectedElements(this.state);
const { renderTopRightUI, renderCustomStats } = this.props; const { renderTopRightUI, renderTopLeftUI, renderCustomStats } = this.props;
const sceneNonce = this.scene.getSceneNonce(); const sceneNonce = this.scene.getSceneNonce();
const { elementsMap, visibleElements } = const { elementsMap, visibleElements } =
@@ -1957,6 +1952,7 @@ class App extends React.Component<AppProps, AppState> {
onPenModeToggle={this.togglePenMode} onPenModeToggle={this.togglePenMode}
onHandToolToggle={this.onHandToolToggle} onHandToolToggle={this.onHandToolToggle}
langCode={getLanguage().code} langCode={getLanguage().code}
renderTopLeftUI={renderTopLeftUI}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats} renderCustomStats={renderCustomStats}
showExitZenModeBtn={ showExitZenModeBtn={
@@ -1969,7 +1965,7 @@ class App extends React.Component<AppProps, AppState> {
!this.state.isLoading && !this.state.isLoading &&
this.state.showWelcomeScreen && this.state.showWelcomeScreen &&
this.state.activeTool.type === this.state.activeTool.type ===
this.defaultSelectionTool && this.state.preferredSelectionTool.type &&
!this.state.zenModeEnabled && !this.state.zenModeEnabled &&
!this.scene.getElementsIncludingDeleted().length !this.scene.getElementsIncludingDeleted().length
} }
@@ -2715,6 +2711,14 @@ class App extends React.Component<AppProps, AppState> {
deleteInvisibleElements: true, deleteInvisibleElements: true,
}); });
const activeTool = scene.appState.activeTool; const activeTool = scene.appState.activeTool;
if (!scene.appState.preferredSelectionTool.initialized) {
scene.appState.preferredSelectionTool = {
type: this.device.editor.isMobile ? "lasso" : "selection",
initialized: true,
};
}
scene.appState = { scene.appState = {
...scene.appState, ...scene.appState,
theme: this.props.theme || scene.appState.theme, theme: this.props.theme || scene.appState.theme,
@@ -2729,12 +2733,13 @@ class App extends React.Component<AppProps, AppState> {
activeTool.type === "selection" activeTool.type === "selection"
? { ? {
...activeTool, ...activeTool,
type: this.defaultSelectionTool, type: scene.appState.preferredSelectionTool.type,
} }
: scene.appState.activeTool, : scene.appState.activeTool,
isLoading: false, isLoading: false,
toast: this.state.toast, toast: this.state.toast,
}; };
if (initialData?.scrollToContent) { if (initialData?.scrollToContent) {
scene.appState = { scene.appState = {
...scene.appState, ...scene.appState,
@@ -2835,6 +2840,8 @@ class App extends React.Component<AppProps, AppState> {
// but not too narrow (> MQ_MAX_WIDTH_MOBILE) // but not too narrow (> MQ_MAX_WIDTH_MOBILE)
this.isTabletBreakpoint(editorWidth, editorHeight) && isMobileOrTablet() this.isTabletBreakpoint(editorWidth, editorHeight) && isMobileOrTablet()
? "compact" ? "compact"
: this.isMobileBreakpoint(editorWidth, editorHeight)
? "mobile"
: "full", : "full",
}); });
@@ -3634,7 +3641,10 @@ class App extends React.Component<AppProps, AppState> {
await this.insertClipboardContent(data, filesList, isPlainPaste); await this.insertClipboardContent(data, filesList, isPlainPaste);
this.setActiveTool({ type: this.defaultSelectionTool }, true); this.setActiveTool(
{ type: this.state.preferredSelectionTool.type },
true,
);
event?.preventDefault(); event?.preventDefault();
}, },
); );
@@ -3780,7 +3790,7 @@ class App extends React.Component<AppProps, AppState> {
} }
}, },
); );
this.setActiveTool({ type: this.defaultSelectionTool }, true); this.setActiveTool({ type: this.state.preferredSelectionTool.type }, true);
if (opts.fitToContent) { if (opts.fitToContent) {
this.scrollToContent(duplicatedElements, { this.scrollToContent(duplicatedElements, {
@@ -3992,7 +4002,7 @@ class App extends React.Component<AppProps, AppState> {
...updateActiveTool( ...updateActiveTool(
this.state, this.state,
prevState.activeTool.locked prevState.activeTool.locked
? { type: this.defaultSelectionTool } ? { type: this.state.preferredSelectionTool.type }
: prevState.activeTool, : prevState.activeTool,
), ),
locked: !prevState.activeTool.locked, locked: !prevState.activeTool.locked,
@@ -4334,7 +4344,12 @@ class App extends React.Component<AppProps, AppState> {
} }
if (appState) { if (appState) {
this.setState(appState); this.setState({
...appState,
// keep existing stylesPanelMode as it needs to be preserved
// or set at startup
stylesPanelMode: this.state.stylesPanelMode,
} as Pick<AppState, K> | null);
} }
if (elements) { if (elements) {
@@ -4992,7 +5007,7 @@ class App extends React.Component<AppProps, AppState> {
if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) { if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
if (this.state.activeTool.type === "laser") { if (this.state.activeTool.type === "laser") {
this.setActiveTool({ type: this.defaultSelectionTool }); this.setActiveTool({ type: this.state.preferredSelectionTool.type });
} else { } else {
this.setActiveTool({ type: "laser" }); this.setActiveTool({ type: "laser" });
} }
@@ -5914,7 +5929,7 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
// we should only be able to double click when mode is selection // we should only be able to double click when mode is selection
if (this.state.activeTool.type !== this.defaultSelectionTool) { if (this.state.activeTool.type !== this.state.preferredSelectionTool.type) {
return; return;
} }
@@ -6930,6 +6945,10 @@ class App extends React.Component<AppProps, AppState> {
this.setAppState({ snapLines: [] }); this.setAppState({ snapLines: [] });
} }
if (this.state.openPopup) {
this.setState({ openPopup: null });
}
this.updateGestureOnPointerDown(event); this.updateGestureOnPointerDown(event);
// if dragging element is freedraw and another pointerdown event occurs // if dragging element is freedraw and another pointerdown event occurs
@@ -8154,7 +8173,7 @@ class App extends React.Component<AppProps, AppState> {
if (!this.state.activeTool.locked) { if (!this.state.activeTool.locked) {
this.setState({ this.setState({
activeTool: updateActiveTool(this.state, { activeTool: updateActiveTool(this.state, {
type: this.defaultSelectionTool, type: this.state.preferredSelectionTool.type,
}), }),
}); });
} }
@@ -9986,7 +10005,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState((prevState) => ({ this.setState((prevState) => ({
newElement: null, newElement: null,
activeTool: updateActiveTool(this.state, { activeTool: updateActiveTool(this.state, {
type: this.defaultSelectionTool, type: this.state.preferredSelectionTool.type,
}), }),
selectedElementIds: makeNextSelectedElementIds( selectedElementIds: makeNextSelectedElementIds(
{ {
@@ -10597,7 +10616,7 @@ class App extends React.Component<AppProps, AppState> {
newElement: null, newElement: null,
suggestedBinding: null, suggestedBinding: null,
activeTool: updateActiveTool(this.state, { activeTool: updateActiveTool(this.state, {
type: this.defaultSelectionTool, type: this.state.preferredSelectionTool.type,
}), }),
}); });
} else { } else {
@@ -10888,7 +10907,7 @@ class App extends React.Component<AppProps, AppState> {
{ {
newElement: null, newElement: null,
activeTool: updateActiveTool(this.state, { activeTool: updateActiveTool(this.state, {
type: this.defaultSelectionTool, type: this.state.preferredSelectionTool.type,
}), }),
}, },
() => { () => {
@@ -11332,7 +11351,7 @@ class App extends React.Component<AppProps, AppState> {
event.nativeEvent.pointerType === "pen" && event.nativeEvent.pointerType === "pen" &&
// always allow if user uses a pen secondary button // always allow if user uses a pen secondary button
event.button !== POINTER_BUTTON.SECONDARY)) && event.button !== POINTER_BUTTON.SECONDARY)) &&
this.state.activeTool.type !== this.defaultSelectionTool this.state.activeTool.type !== this.state.preferredSelectionTool.type
) { ) {
return; return;
} }

View File

@@ -7,6 +7,12 @@
} }
} }
.color-picker__title {
padding: 0 0.5rem;
font-size: 0.875rem;
text-align: left;
}
.color-picker__heading { .color-picker__heading {
padding: 0 0.5rem; padding: 0 0.5rem;
font-size: 0.75rem; font-size: 0.75rem;
@@ -157,6 +163,15 @@
width: 1.625rem; width: 1.625rem;
height: 1.625rem; height: 1.625rem;
} }
&.compact-sizing {
width: var(--mobile-action-button-size);
height: var(--mobile-action-button-size);
}
&.mobile-border {
border: 1px solid var(--mobile-color-border);
}
} }
.color-picker__button__hotkey-label { .color-picker__button__hotkey-label {

View File

@@ -19,7 +19,7 @@ import { useExcalidrawContainer } from "../App";
import { ButtonSeparator } from "../ButtonSeparator"; import { ButtonSeparator } from "../ButtonSeparator";
import { activeEyeDropperAtom } from "../EyeDropper"; import { activeEyeDropperAtom } from "../EyeDropper";
import { PropertiesPopover } from "../PropertiesPopover"; import { PropertiesPopover } from "../PropertiesPopover";
import { backgroundIcon, slashIcon, strokeIcon } from "../icons"; import { slashIcon, strokeIcon } from "../icons";
import { import {
saveCaretPosition, saveCaretPosition,
restoreCaretPosition, restoreCaretPosition,
@@ -216,6 +216,11 @@ const ColorPickerPopupContent = ({
type={type} type={type}
elements={elements} elements={elements}
updateData={updateData} updateData={updateData}
showTitle={
appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile"
}
showHotKey={appState.stylesPanelMode !== "mobile"}
> >
{colorInputJSX} {colorInputJSX}
</Picker> </Picker>
@@ -230,7 +235,7 @@ const ColorPickerTrigger = ({
label, label,
color, color,
type, type,
compactMode = false, stylesPanelMode,
mode = "background", mode = "background",
onToggle, onToggle,
editingTextElement, editingTextElement,
@@ -238,7 +243,7 @@ const ColorPickerTrigger = ({
color: string | null; color: string | null;
label: string; label: string;
type: ColorPickerType; type: ColorPickerType;
compactMode?: boolean; stylesPanelMode?: AppState["stylesPanelMode"];
mode?: "background" | "stroke"; mode?: "background" | "stroke";
onToggle: () => void; onToggle: () => void;
editingTextElement?: boolean; editingTextElement?: boolean;
@@ -263,6 +268,9 @@ const ColorPickerTrigger = ({
"is-transparent": !color || color === "transparent", "is-transparent": !color || color === "transparent",
"has-outline": "has-outline":
!color || !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD), !color || !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
"compact-sizing":
stylesPanelMode === "compact" || stylesPanelMode === "mobile",
"mobile-border": stylesPanelMode === "mobile",
})} })}
aria-label={label} aria-label={label}
style={color ? { "--swatch-color": color } : undefined} style={color ? { "--swatch-color": color } : undefined}
@@ -275,20 +283,10 @@ const ColorPickerTrigger = ({
onClick={handleClick} onClick={handleClick}
> >
<div className="color-picker__button-outline">{!color && slashIcon}</div> <div className="color-picker__button-outline">{!color && slashIcon}</div>
{compactMode && color && ( {(stylesPanelMode === "compact" || stylesPanelMode === "mobile") &&
<div className="color-picker__button-background"> color &&
{mode === "background" ? ( mode === "stroke" && (
<span <div className="color-picker__button-background">
style={{
color:
color && isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD)
? "#fff"
: "#111",
}}
>
{backgroundIcon}
</span>
) : (
<span <span
style={{ style={{
color: color:
@@ -299,9 +297,8 @@ const ColorPickerTrigger = ({
> >
{strokeIcon} {strokeIcon}
</span> </span>
)} </div>
</div> )}
)}
</Popover.Trigger> </Popover.Trigger>
); );
}; };
@@ -316,12 +313,15 @@ export const ColorPicker = ({
topPicks, topPicks,
updateData, updateData,
appState, appState,
compactMode = false,
}: ColorPickerProps) => { }: ColorPickerProps) => {
const openRef = useRef(appState.openPopup); const openRef = useRef(appState.openPopup);
useEffect(() => { useEffect(() => {
openRef.current = appState.openPopup; openRef.current = appState.openPopup;
}, [appState.openPopup]); }, [appState.openPopup]);
const compactMode =
appState.stylesPanelMode === "compact" ||
appState.stylesPanelMode === "mobile";
return ( return (
<div> <div>
<div <div
@@ -353,7 +353,7 @@ export const ColorPicker = ({
color={color} color={color}
label={label} label={label}
type={type} type={type}
compactMode={compactMode} stylesPanelMode={appState.stylesPanelMode}
mode={type === "elementStroke" ? "stroke" : "background"} mode={type === "elementStroke" ? "stroke" : "background"}
editingTextElement={!!appState.editingTextElement} editingTextElement={!!appState.editingTextElement}
onToggle={() => { onToggle={() => {

View File

@@ -37,8 +37,10 @@ interface PickerProps {
palette: ColorPaletteCustom; palette: ColorPaletteCustom;
updateData: (formData?: any) => void; updateData: (formData?: any) => void;
children?: React.ReactNode; children?: React.ReactNode;
showTitle?: boolean;
onEyeDropperToggle: (force?: boolean) => void; onEyeDropperToggle: (force?: boolean) => void;
onEscape: (event: React.KeyboardEvent | KeyboardEvent) => void; onEscape: (event: React.KeyboardEvent | KeyboardEvent) => void;
showHotKey?: boolean;
} }
export const Picker = React.forwardRef( export const Picker = React.forwardRef(
@@ -51,11 +53,21 @@ export const Picker = React.forwardRef(
palette, palette,
updateData, updateData,
children, children,
showTitle,
onEyeDropperToggle, onEyeDropperToggle,
onEscape, onEscape,
showHotKey = true,
}: PickerProps, }: PickerProps,
ref, ref,
) => { ) => {
const title = showTitle
? type === "elementStroke"
? t("labels.stroke")
: type === "elementBackground"
? t("labels.background")
: null
: null;
const [customColors] = React.useState(() => { const [customColors] = React.useState(() => {
if (type === "canvasBackground") { if (type === "canvasBackground") {
return []; return [];
@@ -154,6 +166,8 @@ export const Picker = React.forwardRef(
// to allow focusing by clicking but not by tabbing // to allow focusing by clicking but not by tabbing
tabIndex={-1} tabIndex={-1}
> >
{title && <div className="color-picker__title">{title}</div>}
{!!customColors.length && ( {!!customColors.length && (
<div> <div>
<PickerHeading> <PickerHeading>
@@ -175,12 +189,18 @@ export const Picker = React.forwardRef(
palette={palette} palette={palette}
onChange={onChange} onChange={onChange}
activeShade={activeShade} activeShade={activeShade}
showHotKey={showHotKey}
/> />
</div> </div>
<div> <div>
<PickerHeading>{t("colorPicker.shades")}</PickerHeading> <PickerHeading>{t("colorPicker.shades")}</PickerHeading>
<ShadeList color={color} onChange={onChange} palette={palette} /> <ShadeList
color={color}
onChange={onChange}
palette={palette}
showHotKey={showHotKey}
/>
</div> </div>
{children} {children}
</div> </div>

View File

@@ -20,6 +20,7 @@ interface PickerColorListProps {
color: string | null; color: string | null;
onChange: (color: string) => void; onChange: (color: string) => void;
activeShade: number; activeShade: number;
showHotKey?: boolean;
} }
const PickerColorList = ({ const PickerColorList = ({
@@ -27,6 +28,7 @@ const PickerColorList = ({
color, color,
onChange, onChange,
activeShade, activeShade,
showHotKey = true,
}: PickerColorListProps) => { }: PickerColorListProps) => {
const colorObj = getColorNameAndShadeFromColor({ const colorObj = getColorNameAndShadeFromColor({
color, color,
@@ -82,7 +84,7 @@ const PickerColorList = ({
key={key} key={key}
> >
<div className="color-picker__button-outline" /> <div className="color-picker__button-outline" />
<HotkeyLabel color={color} keyLabel={keybinding} /> {showHotKey && <HotkeyLabel color={color} keyLabel={keybinding} />}
</button> </button>
); );
})} })}

View File

@@ -16,9 +16,15 @@ interface ShadeListProps {
color: string | null; color: string | null;
onChange: (color: string) => void; onChange: (color: string) => void;
palette: ColorPaletteCustom; palette: ColorPaletteCustom;
showHotKey?: boolean;
} }
export const ShadeList = ({ color, onChange, palette }: ShadeListProps) => { export const ShadeList = ({
color,
onChange,
palette,
showHotKey,
}: ShadeListProps) => {
const colorObj = getColorNameAndShadeFromColor({ const colorObj = getColorNameAndShadeFromColor({
color: color || "transparent", color: color || "transparent",
palette, palette,
@@ -67,7 +73,9 @@ export const ShadeList = ({ color, onChange, palette }: ShadeListProps) => {
}} }}
> >
<div className="color-picker__button-outline" /> <div className="color-picker__button-outline" />
<HotkeyLabel color={color} keyLabel={i + 1} isShade /> {showHotKey && (
<HotkeyLabel color={color} keyLabel={i + 1} isShade />
)}
</button> </button>
))} ))}
</div> </div>

View File

@@ -1,5 +1,8 @@
.excalidraw { .excalidraw {
.ExcalidrawLogo { .ExcalidrawLogo {
--logo-icon--mobile: 1rem;
--logo-text--mobile: 0.75rem;
--logo-icon--xs: 2rem; --logo-icon--xs: 2rem;
--logo-text--xs: 1.5rem; --logo-text--xs: 1.5rem;
@@ -30,6 +33,17 @@
color: var(--color-logo-text); color: var(--color-logo-text);
} }
&.is-mobile {
.ExcalidrawLogo-icon {
height: var(--logo-icon--mobile);
}
.ExcalidrawLogo-text {
height: var(--logo-text--mobile);
margin-left: 0.5rem;
}
}
&.is-xs { &.is-xs {
.ExcalidrawLogo-icon { .ExcalidrawLogo-icon {
height: var(--logo-icon--xs); height: var(--logo-icon--xs);

View File

@@ -41,7 +41,7 @@ const LogoText = () => (
</svg> </svg>
); );
type LogoSize = "xs" | "small" | "normal" | "large" | "custom"; type LogoSize = "xs" | "small" | "normal" | "large" | "custom" | "mobile";
interface LogoProps { interface LogoProps {
size?: LogoSize; size?: LogoSize;

View File

@@ -106,6 +106,7 @@ export const FontPicker = React.memo(
<FontPickerTrigger <FontPickerTrigger
selectedFontFamily={selectedFontFamily} selectedFontFamily={selectedFontFamily}
isOpened={isOpened} isOpened={isOpened}
compactMode={compactMode}
/> />
{isOpened && ( {isOpened && (
<FontPickerList <FontPickerList

View File

@@ -338,11 +338,13 @@ export const FontPickerList = React.memo(
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
preventAutoFocusOnTouch={!!app.state.editingTextElement} preventAutoFocusOnTouch={!!app.state.editingTextElement}
> >
<QuickSearch {app.state.stylesPanelMode === "full" && (
ref={inputRef} <QuickSearch
placeholder={t("quickSearch.placeholder")} ref={inputRef}
onChange={debounce(setSearchTerm, 20)} placeholder={t("quickSearch.placeholder")}
/> onChange={debounce(setSearchTerm, 20)}
/>
)}
<ScrollableList <ScrollableList
className="dropdown-menu fonts manual-hover" className="dropdown-menu fonts manual-hover"
placeholder={t("fontList.empty")} placeholder={t("fontList.empty")}

View File

@@ -1,5 +1,7 @@
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { MOBILE_ACTION_BUTTON_BG } from "@excalidraw/common";
import type { FontFamilyValues } from "@excalidraw/element/types"; import type { FontFamilyValues } from "@excalidraw/element/types";
import { t } from "../../i18n"; import { t } from "../../i18n";
@@ -11,14 +13,24 @@ import { useExcalidrawSetAppState } from "../App";
interface FontPickerTriggerProps { interface FontPickerTriggerProps {
selectedFontFamily: FontFamilyValues | null; selectedFontFamily: FontFamilyValues | null;
isOpened?: boolean; isOpened?: boolean;
compactMode?: boolean;
} }
export const FontPickerTrigger = ({ export const FontPickerTrigger = ({
selectedFontFamily, selectedFontFamily,
isOpened = false, isOpened = false,
compactMode = false,
}: FontPickerTriggerProps) => { }: FontPickerTriggerProps) => {
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const compactStyle = compactMode
? {
...MOBILE_ACTION_BUTTON_BG,
width: "2rem",
height: "2rem",
}
: {};
return ( return (
<Popover.Trigger asChild> <Popover.Trigger asChild>
<div data-openpopup="fontFamily" className="properties-trigger"> <div data-openpopup="fontFamily" className="properties-trigger">
@@ -37,6 +49,7 @@ export const FontPickerTrigger = ({
}} }}
style={{ style={{
border: "none", border: "none",
...compactStyle,
}} }}
/> />
</div> </div>

View File

@@ -18,7 +18,7 @@ type LockIconProps = {
export const HandButton = (props: LockIconProps) => { export const HandButton = (props: LockIconProps) => {
return ( return (
<ToolButton <ToolButton
className={clsx("Shape", { fillable: false })} className={clsx("Shape", { fillable: false, active: props.checked })}
type="radio" type="radio"
icon={handIcon} icon={handIcon}
name="editor-current-shape" name="editor-current-shape"

View File

@@ -152,15 +152,13 @@ function Picker<T>({
); );
}; };
const isMobile = device.editor.isMobile;
return ( return (
<Popover.Content <Popover.Content
side={ side={isMobile ? "right" : "bottom"}
device.editor.isMobile && !device.viewport.isLandscape
? "top"
: "bottom"
}
align="start" align="start"
sideOffset={12} sideOffset={isMobile ? 8 : 12}
style={{ zIndex: "var(--zIndex-popup)" }} style={{ zIndex: "var(--zIndex-popup)" }}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >

View File

@@ -91,6 +91,7 @@ interface LayerUIProps {
onPenModeToggle: AppClassProperties["togglePenMode"]; onPenModeToggle: AppClassProperties["togglePenMode"];
showExitZenModeBtn: boolean; showExitZenModeBtn: boolean;
langCode: Language["code"]; langCode: Language["code"];
renderTopLeftUI?: ExcalidrawProps["renderTopLeftUI"];
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
UIOptions: AppProps["UIOptions"]; UIOptions: AppProps["UIOptions"];
@@ -149,6 +150,7 @@ const LayerUI = ({
onHandToolToggle, onHandToolToggle,
onPenModeToggle, onPenModeToggle,
showExitZenModeBtn, showExitZenModeBtn,
renderTopLeftUI,
renderTopRightUI, renderTopRightUI,
renderCustomStats, renderCustomStats,
UIOptions, UIOptions,
@@ -366,7 +368,7 @@ const LayerUI = ({
/> />
<ShapesSwitcher <ShapesSwitcher
appState={appState} setAppState={setAppState}
activeTool={appState.activeTool} activeTool={appState.activeTool}
UIOptions={UIOptions} UIOptions={UIOptions}
app={app} app={app}
@@ -582,13 +584,11 @@ const LayerUI = ({
renderJSONExportDialog={renderJSONExportDialog} renderJSONExportDialog={renderJSONExportDialog}
renderImageExportDialog={renderImageExportDialog} renderImageExportDialog={renderImageExportDialog}
setAppState={setAppState} setAppState={setAppState}
onLockToggle={onLockToggle}
onHandToolToggle={onHandToolToggle} onHandToolToggle={onHandToolToggle}
onPenModeToggle={onPenModeToggle} onPenModeToggle={onPenModeToggle}
renderTopLeftUI={renderTopLeftUI}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats}
renderSidebars={renderSidebars} renderSidebars={renderSidebars}
device={device}
renderWelcomeScreen={renderWelcomeScreen} renderWelcomeScreen={renderWelcomeScreen}
UIOptions={UIOptions} UIOptions={UIOptions}
/> />

View File

@@ -1,32 +1,23 @@
import React from "react"; import React from "react";
import { showSelectedShapeActions } from "@excalidraw/element";
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types"; import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
import { isHandToolActive } from "../appState";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../context/tunnels";
import { t } from "../i18n"; import { t } from "../i18n";
import { calculateScrollCenter } from "../scene"; import { calculateScrollCenter } from "../scene";
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars"; import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { MobileShapeActions } from "./Actions";
import { MobileToolBar } from "./MobileToolBar";
import { FixedSideContainer } from "./FixedSideContainer"; import { FixedSideContainer } from "./FixedSideContainer";
import { HandButton } from "./HandButton";
import { HintViewer } from "./HintViewer";
import { Island } from "./Island"; import { Island } from "./Island";
import { LockButton } from "./LockButton";
import { PenModeButton } from "./PenModeButton";
import { Section } from "./Section";
import Stack from "./Stack";
import type { ActionManager } from "../actions/manager"; import type { ActionManager } from "../actions/manager";
import type { import type {
AppClassProperties, AppClassProperties,
AppProps, AppProps,
AppState, AppState,
Device,
ExcalidrawProps,
UIAppState, UIAppState,
} from "../types"; } from "../types";
import type { JSX } from "react"; import type { JSX } from "react";
@@ -38,7 +29,6 @@ type MobileMenuProps = {
renderImageExportDialog: () => React.ReactNode; renderImageExportDialog: () => React.ReactNode;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
onLockToggle: () => void;
onHandToolToggle: () => void; onHandToolToggle: () => void;
onPenModeToggle: AppClassProperties["togglePenMode"]; onPenModeToggle: AppClassProperties["togglePenMode"];
@@ -46,9 +36,11 @@ type MobileMenuProps = {
isMobile: boolean, isMobile: boolean,
appState: UIAppState, appState: UIAppState,
) => JSX.Element | null; ) => JSX.Element | null;
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderTopLeftUI?: (
isMobile: boolean,
appState: UIAppState,
) => JSX.Element | null;
renderSidebars: () => JSX.Element | null; renderSidebars: () => JSX.Element | null;
device: Device;
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
UIOptions: AppProps["UIOptions"]; UIOptions: AppProps["UIOptions"];
app: AppClassProperties; app: AppClassProperties;
@@ -59,14 +51,10 @@ export const MobileMenu = ({
elements, elements,
actionManager, actionManager,
setAppState, setAppState,
onLockToggle,
onHandToolToggle, onHandToolToggle,
onPenModeToggle, renderTopLeftUI,
renderTopRightUI, renderTopRightUI,
renderCustomStats,
renderSidebars, renderSidebars,
device,
renderWelcomeScreen, renderWelcomeScreen,
UIOptions, UIOptions,
app, app,
@@ -76,141 +64,98 @@ export const MobileMenu = ({
MainMenuTunnel, MainMenuTunnel,
DefaultSidebarTriggerTunnel, DefaultSidebarTriggerTunnel,
} = useTunnels(); } = useTunnels();
const renderToolbar = () => { const renderAppTopBar = () => {
return ( const topRightUI = renderTopRightUI?.(true, appState) ?? (
<FixedSideContainer side="top" className="App-top-bar"> <DefaultSidebarTriggerTunnel.Out />
{renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />} );
<Section heading="shapes">
{(heading: React.ReactNode) => ( const topLeftUI = (
<Stack.Col gap={4} align="center"> <div className="excalidraw-ui-top-left">
<Stack.Row gap={1} className="App-toolbar-container"> {renderTopLeftUI?.(true, appState)}
<Island padding={1} className="App-toolbar App-toolbar--mobile"> <MainMenuTunnel.Out />
{heading} </div>
<Stack.Row gap={1}>
<ShapesSwitcher
appState={appState}
activeTool={appState.activeTool}
UIOptions={UIOptions}
app={app}
/>
</Stack.Row>
</Island>
{renderTopRightUI && renderTopRightUI(true, appState)}
<div className="mobile-misc-tools-container">
{!appState.viewModeEnabled &&
appState.openDialog?.name !== "elementLinkSelector" && (
<DefaultSidebarTriggerTunnel.Out />
)}
<PenModeButton
checked={appState.penMode}
onChange={() => onPenModeToggle(null)}
title={t("toolBar.penMode")}
isMobile
penDetected={appState.penDetected}
/>
<LockButton
checked={appState.activeTool.locked}
onChange={onLockToggle}
title={t("toolBar.lock")}
isMobile
/>
<HandButton
checked={isHandToolActive(appState)}
onChange={() => onHandToolToggle()}
title={t("toolBar.hand")}
isMobile
/>
</div>
</Stack.Row>
</Stack.Col>
)}
</Section>
<HintViewer
appState={appState}
isMobile={true}
device={device}
app={app}
/>
</FixedSideContainer>
); );
};
const renderAppToolbar = () => {
if ( if (
appState.viewModeEnabled || appState.viewModeEnabled ||
appState.openDialog?.name === "elementLinkSelector" appState.openDialog?.name === "elementLinkSelector"
) { ) {
return ( return <div className="App-toolbar-content">{topLeftUI}</div>;
<div className="App-toolbar-content">
<MainMenuTunnel.Out />
</div>
);
} }
return ( return (
<div className="App-toolbar-content"> <div
<MainMenuTunnel.Out /> className="App-toolbar-content"
{actionManager.renderAction("toggleEditMenu")} style={{
{actionManager.renderAction( display: "flex",
appState.multiElement ? "finalize" : "duplicateSelection", flexDirection: "row",
)} justifyContent: "space-between",
{actionManager.renderAction("deleteSelectedElements")} }}
<div> >
{actionManager.renderAction("undo")} {topLeftUI}
{actionManager.renderAction("redo")} {topRightUI}
</div>
</div> </div>
); );
}; };
const renderToolbar = () => {
return (
<MobileToolBar
app={app}
onHandToolToggle={onHandToolToggle}
setAppState={setAppState}
/>
);
};
return ( return (
<> <>
{renderSidebars()} {renderSidebars()}
{!appState.viewModeEnabled && {/* welcome screen, bottom bar, and top bar all have the same z-index */}
appState.openDialog?.name !== "elementLinkSelector" && {/* ordered in this reverse order so that top bar is on top */}
renderToolbar()} <div className="App-welcome-screen">
{renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />}
</div>
<div <div
className="App-bottom-bar" className="App-bottom-bar"
style={{ style={{
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2, marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN,
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
}} }}
> >
<Island padding={0}> <MobileShapeActions
{appState.openMenu === "shape" && appState={appState}
!appState.viewModeEnabled && elementsMap={app.scene.getNonDeletedElementsMap()}
appState.openDialog?.name !== "elementLinkSelector" && renderAction={actionManager.renderAction}
showSelectedShapeActions(appState, elements) ? ( app={app}
<Section className="App-mobile-menu" heading="selectedShapeActions"> setAppState={setAppState}
<SelectedShapeActions />
appState={appState}
elementsMap={app.scene.getNonDeletedElementsMap()} <Island className="App-toolbar">
renderAction={actionManager.renderAction} {!appState.viewModeEnabled &&
app={app} appState.openDialog?.name !== "elementLinkSelector" &&
/> renderToolbar()}
</Section> {appState.scrolledOutside &&
) : null} !appState.openMenu &&
<footer className="App-toolbar"> !appState.openSidebar && (
{renderAppToolbar()} <button
{appState.scrolledOutside && type="button"
!appState.openMenu && className="scroll-back-to-content"
!appState.openSidebar && ( onClick={() => {
<button setAppState((appState) => ({
type="button" ...calculateScrollCenter(elements, appState),
className="scroll-back-to-content" }));
onClick={() => { }}
setAppState((appState) => ({ >
...calculateScrollCenter(elements, appState), {t("buttons.scrollBackToContent")}
})); </button>
}} )}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</footer>
</Island> </Island>
</div> </div>
<FixedSideContainer side="top" className="App-top-bar">
{renderAppTopBar()}
</FixedSideContainer>
</> </>
); );
}; };

View File

@@ -0,0 +1,78 @@
@import "open-color/open-color.scss";
@import "../css/variables.module.scss";
.excalidraw {
.mobile-toolbar {
display: flex;
flex: 1;
align-items: center;
padding: 0px;
gap: 4px;
border-radius: var(--space-factor);
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
justify-content: space-between;
}
.mobile-toolbar::-webkit-scrollbar {
display: none;
}
.mobile-toolbar .ToolIcon {
min-width: 2rem;
min-height: 2rem;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.ToolIcon__icon {
width: 2.25rem;
height: 2.25rem;
&:hover {
background-color: transparent;
}
}
&.active {
background: var(
--color-surface-primary-container,
var(--island-bg-color)
);
border-color: var(--button-active-border, var(--color-primary-darkest));
}
svg {
width: 1rem;
height: 1rem;
}
}
.mobile-toolbar .App-toolbar__extra-tools-dropdown {
min-width: 160px;
z-index: var(--zIndex-layerUI);
}
.mobile-toolbar-separator {
width: 1px;
height: 24px;
background: var(--default-border-color);
margin: 0 2px;
flex-shrink: 0;
}
.mobile-toolbar-undo {
display: flex;
align-items: center;
}
.mobile-toolbar-undo .ToolIcon {
min-width: 32px;
min-height: 32px;
width: 32px;
height: 32px;
}
}

View File

@@ -0,0 +1,471 @@
import { useState, useEffect, useRef } from "react";
import clsx from "clsx";
import { KEYS, capitalizeString } from "@excalidraw/common";
import { trackEvent } from "../analytics";
import { t } from "../i18n";
import { isHandToolActive } from "../appState";
import { useTunnels } from "../context/tunnels";
import { HandButton } from "./HandButton";
import { ToolButton } from "./ToolButton";
import DropdownMenu from "./dropdownMenu/DropdownMenu";
import { ToolPopover } from "./ToolPopover";
import {
SelectionIcon,
FreedrawIcon,
EraserIcon,
RectangleIcon,
ArrowIcon,
extraToolsIcon,
DiamondIcon,
EllipseIcon,
LineIcon,
TextIcon,
ImageIcon,
frameToolIcon,
EmbedIcon,
laserPointerToolIcon,
LassoIcon,
mermaidLogoIcon,
MagicIcon,
} from "./icons";
import "./ToolIcon.scss";
import "./MobileToolBar.scss";
import type { AppClassProperties, ToolType, UIAppState } from "../types";
const SHAPE_TOOLS = [
{
type: "rectangle",
icon: RectangleIcon,
title: capitalizeString(t("toolBar.rectangle")),
},
{
type: "diamond",
icon: DiamondIcon,
title: capitalizeString(t("toolBar.diamond")),
},
{
type: "ellipse",
icon: EllipseIcon,
title: capitalizeString(t("toolBar.ellipse")),
},
] as const;
const SELECTION_TOOLS = [
{
type: "selection",
icon: SelectionIcon,
title: capitalizeString(t("toolBar.selection")),
},
{
type: "lasso",
icon: LassoIcon,
title: capitalizeString(t("toolBar.lasso")),
},
] as const;
const LINEAR_ELEMENT_TOOLS = [
{
type: "arrow",
icon: ArrowIcon,
title: capitalizeString(t("toolBar.arrow")),
},
{ type: "line", icon: LineIcon, title: capitalizeString(t("toolBar.line")) },
] as const;
type MobileToolBarProps = {
app: AppClassProperties;
onHandToolToggle: () => void;
setAppState: React.Component<any, UIAppState>["setState"];
};
export const MobileToolBar = ({
app,
onHandToolToggle,
setAppState,
}: MobileToolBarProps) => {
const activeTool = app.state.activeTool;
const [isOtherShapesMenuOpen, setIsOtherShapesMenuOpen] = useState(false);
const [lastActiveGenericShape, setLastActiveGenericShape] = useState<
"rectangle" | "diamond" | "ellipse"
>("rectangle");
const [lastActiveLinearElement, setLastActiveLinearElement] = useState<
"arrow" | "line"
>("arrow");
const toolbarRef = useRef<HTMLDivElement>(null);
// keep lastActiveGenericShape in sync with active tool if user switches via other UI
useEffect(() => {
if (
activeTool.type === "rectangle" ||
activeTool.type === "diamond" ||
activeTool.type === "ellipse"
) {
setLastActiveGenericShape(activeTool.type);
}
}, [activeTool.type]);
// keep lastActiveLinearElement in sync with active tool if user switches via other UI
useEffect(() => {
if (activeTool.type === "arrow" || activeTool.type === "line") {
setLastActiveLinearElement(activeTool.type);
}
}, [activeTool.type]);
const frameToolSelected = activeTool.type === "frame";
const laserToolSelected = activeTool.type === "laser";
const embeddableToolSelected = activeTool.type === "embeddable";
const { TTDDialogTriggerTunnel } = useTunnels();
const handleToolChange = (toolType: string, pointerType?: string) => {
if (app.state.activeTool.type !== toolType) {
trackEvent("toolbar", toolType, "ui");
}
if (toolType === "selection") {
if (app.state.activeTool.type === "selection") {
// Toggle selection tool behavior if needed
} else {
app.setActiveTool({ type: "selection" });
}
} else {
app.setActiveTool({ type: toolType as ToolType });
}
};
const toolbarWidth =
toolbarRef.current?.getBoundingClientRect()?.width ?? 0 - 8;
const WIDTH = 36;
const GAP = 4;
// hand, selection, freedraw, eraser, rectangle, arrow, others
const MIN_TOOLS = 7;
const MIN_WIDTH = MIN_TOOLS * WIDTH + (MIN_TOOLS - 1) * GAP;
const ADDITIONAL_WIDTH = WIDTH + GAP;
const showTextToolOutside = toolbarWidth >= MIN_WIDTH + 1 * ADDITIONAL_WIDTH;
const showImageToolOutside = toolbarWidth >= MIN_WIDTH + 2 * ADDITIONAL_WIDTH;
const showFrameToolOutside = toolbarWidth >= MIN_WIDTH + 3 * ADDITIONAL_WIDTH;
const extraTools = [
"text",
"frame",
"embeddable",
"laser",
"magicframe",
].filter((tool) => {
if (showImageToolOutside && tool === "image") {
return false;
}
if (showFrameToolOutside && tool === "frame") {
return false;
}
return true;
});
const extraToolSelected = extraTools.includes(activeTool.type);
const extraIcon = extraToolSelected
? activeTool.type === "frame"
? frameToolIcon
: activeTool.type === "embeddable"
? EmbedIcon
: activeTool.type === "laser"
? laserPointerToolIcon
: activeTool.type === "text"
? TextIcon
: activeTool.type === "magicframe"
? MagicIcon
: extraToolsIcon
: extraToolsIcon;
return (
<div className="mobile-toolbar" ref={toolbarRef}>
{/* Hand Tool */}
<HandButton
checked={isHandToolActive(app.state)}
onChange={onHandToolToggle}
title={t("toolBar.hand")}
isMobile
/>
{/* Selection Tool */}
<ToolPopover
app={app}
options={SELECTION_TOOLS}
activeTool={activeTool}
defaultOption={app.state.preferredSelectionTool.type}
namePrefix="selectionType"
title={capitalizeString(t("toolBar.selection"))}
data-testid="toolbar-selection"
onToolChange={(type: string) => {
if (type === "selection" || type === "lasso") {
app.setActiveTool({ type });
setAppState({
preferredSelectionTool: { type, initialized: true },
});
}
}}
displayedOption={
SELECTION_TOOLS.find(
(tool) => tool.type === app.state.preferredSelectionTool.type,
) || SELECTION_TOOLS[0]
}
/>
{/* Free Draw */}
<ToolButton
className={clsx({
active: activeTool.type === "freedraw",
})}
type="radio"
icon={FreedrawIcon}
checked={activeTool.type === "freedraw"}
name="editor-current-shape"
title={`${capitalizeString(t("toolBar.freedraw"))}`}
aria-label={capitalizeString(t("toolBar.freedraw"))}
data-testid="toolbar-freedraw"
onChange={() => handleToolChange("freedraw")}
/>
{/* Eraser */}
<ToolButton
className={clsx({
active: activeTool.type === "eraser",
})}
type="radio"
icon={EraserIcon}
checked={activeTool.type === "eraser"}
name="editor-current-shape"
title={`${capitalizeString(t("toolBar.eraser"))}`}
aria-label={capitalizeString(t("toolBar.eraser"))}
data-testid="toolbar-eraser"
onChange={() => handleToolChange("eraser")}
/>
{/* Rectangle */}
<ToolPopover
app={app}
options={SHAPE_TOOLS}
activeTool={activeTool}
defaultOption={lastActiveGenericShape}
namePrefix="shapeType"
title={capitalizeString(
t(
lastActiveGenericShape === "rectangle"
? "toolBar.rectangle"
: lastActiveGenericShape === "diamond"
? "toolBar.diamond"
: lastActiveGenericShape === "ellipse"
? "toolBar.ellipse"
: "toolBar.rectangle",
),
)}
data-testid="toolbar-rectangle"
onToolChange={(type: string) => {
if (
type === "rectangle" ||
type === "diamond" ||
type === "ellipse"
) {
setLastActiveGenericShape(type);
app.setActiveTool({ type });
}
}}
displayedOption={
SHAPE_TOOLS.find((tool) => tool.type === lastActiveGenericShape) ||
SHAPE_TOOLS[0]
}
/>
{/* Arrow/Line */}
<ToolPopover
app={app}
options={LINEAR_ELEMENT_TOOLS}
activeTool={activeTool}
defaultOption={lastActiveLinearElement}
namePrefix="linearElementType"
title={capitalizeString(
t(
lastActiveLinearElement === "arrow"
? "toolBar.arrow"
: "toolBar.line",
),
)}
data-testid="toolbar-arrow"
fillable={true}
onToolChange={(type: string) => {
if (type === "arrow" || type === "line") {
setLastActiveLinearElement(type);
app.setActiveTool({ type });
}
}}
displayedOption={
LINEAR_ELEMENT_TOOLS.find(
(tool) => tool.type === lastActiveLinearElement,
) || LINEAR_ELEMENT_TOOLS[0]
}
/>
{/* Text Tool */}
{showTextToolOutside && (
<ToolButton
className={clsx({
active: activeTool.type === "text",
})}
type="radio"
icon={TextIcon}
checked={activeTool.type === "text"}
name="editor-current-shape"
title={`${capitalizeString(t("toolBar.text"))}`}
aria-label={capitalizeString(t("toolBar.text"))}
data-testid="toolbar-text"
onChange={() => handleToolChange("text")}
/>
)}
{/* Image */}
{showImageToolOutside && (
<ToolButton
className={clsx({
active: activeTool.type === "image",
})}
type="radio"
icon={ImageIcon}
checked={activeTool.type === "image"}
name="editor-current-shape"
title={`${capitalizeString(t("toolBar.image"))}`}
aria-label={capitalizeString(t("toolBar.image"))}
data-testid="toolbar-image"
onChange={() => handleToolChange("image")}
/>
)}
{/* Frame Tool */}
{showFrameToolOutside && (
<ToolButton
className={clsx({ active: frameToolSelected })}
type="radio"
icon={frameToolIcon}
checked={frameToolSelected}
name="editor-current-shape"
title={`${capitalizeString(t("toolBar.frame"))}`}
aria-label={capitalizeString(t("toolBar.frame"))}
data-testid="toolbar-frame"
onChange={() => handleToolChange("frame")}
/>
)}
{/* Other Shapes */}
<DropdownMenu open={isOtherShapesMenuOpen} placement="top">
<DropdownMenu.Trigger
className={clsx(
"App-toolbar__extra-tools-trigger App-toolbar__extra-tools-trigger--mobile",
{
"App-toolbar__extra-tools-trigger--selected":
extraToolSelected || isOtherShapesMenuOpen,
},
)}
onToggle={() => setIsOtherShapesMenuOpen(!isOtherShapesMenuOpen)}
title={t("toolBar.extraTools")}
style={{
width: WIDTH,
height: WIDTH,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{extraIcon}
</DropdownMenu.Trigger>
<DropdownMenu.Content
onClickOutside={() => setIsOtherShapesMenuOpen(false)}
onSelect={() => setIsOtherShapesMenuOpen(false)}
className="App-toolbar__extra-tools-dropdown"
>
{!showTextToolOutside && (
<DropdownMenu.Item
onSelect={() => app.setActiveTool({ type: "text" })}
icon={TextIcon}
shortcut={KEYS.T.toLocaleUpperCase()}
data-testid="toolbar-text"
selected={activeTool.type === "text"}
>
{t("toolBar.text")}
</DropdownMenu.Item>
)}
{!showImageToolOutside && (
<DropdownMenu.Item
onSelect={() => app.setActiveTool({ type: "image" })}
icon={ImageIcon}
data-testid="toolbar-image"
selected={activeTool.type === "image"}
>
{t("toolBar.image")}
</DropdownMenu.Item>
)}
{!showFrameToolOutside && (
<DropdownMenu.Item
onSelect={() => app.setActiveTool({ type: "frame" })}
icon={frameToolIcon}
shortcut={KEYS.F.toLocaleUpperCase()}
data-testid="toolbar-frame"
selected={frameToolSelected}
>
{t("toolBar.frame")}
</DropdownMenu.Item>
)}
<DropdownMenu.Item
onSelect={() => app.setActiveTool({ type: "embeddable" })}
icon={EmbedIcon}
data-testid="toolbar-embeddable"
selected={embeddableToolSelected}
>
{t("toolBar.embeddable")}
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => app.setActiveTool({ type: "laser" })}
icon={laserPointerToolIcon}
data-testid="toolbar-laser"
selected={laserToolSelected}
shortcut={KEYS.K.toLocaleUpperCase()}
>
{t("toolBar.laser")}
</DropdownMenu.Item>
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
Generate
</div>
{app.props.aiEnabled !== false && <TTDDialogTriggerTunnel.Out />}
<DropdownMenu.Item
onSelect={() => app.setOpenDialog({ name: "ttd", tab: "mermaid" })}
icon={mermaidLogoIcon}
data-testid="toolbar-embeddable"
>
{t("toolBar.mermaidToExcalidraw")}
</DropdownMenu.Item>
{app.props.aiEnabled !== false && app.plugins.diagramToCode && (
<>
<DropdownMenu.Item
onSelect={() => app.onMagicframeToolSelect()}
icon={MagicIcon}
data-testid="toolbar-magicframe"
>
{t("toolBar.magicframe")}
<DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>
</DropdownMenu.Item>
</>
)}
</DropdownMenu.Content>
</DropdownMenu>
</div>
);
};

View File

@@ -1,12 +1,11 @@
import { trackEvent } from "../../analytics"; import { trackEvent } from "../../analytics";
import { useTunnels } from "../../context/tunnels"; import { useTunnels } from "../../context/tunnels";
import { t } from "../../i18n"; import { useI18n } from "../../i18n";
import { useExcalidrawSetAppState } from "../App"; import { useExcalidrawSetAppState } from "../App";
import DropdownMenu from "../dropdownMenu/DropdownMenu"; import DropdownMenu from "../dropdownMenu/DropdownMenu";
import { brainIcon } from "../icons"; import { brainIcon } from "../icons";
import type { ReactNode } from "react"; import type { JSX, ReactNode } from "react";
import type { JSX } from "react";
export const TTDDialogTrigger = ({ export const TTDDialogTrigger = ({
children, children,
@@ -15,6 +14,7 @@ export const TTDDialogTrigger = ({
children?: ReactNode; children?: ReactNode;
icon?: JSX.Element; icon?: JSX.Element;
}) => { }) => {
const { t } = useI18n();
const { TTDDialogTriggerTunnel } = useTunnels(); const { TTDDialogTriggerTunnel } = useTunnels();
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();

View File

@@ -0,0 +1,18 @@
@import "../css/variables.module.scss";
.excalidraw {
.tool-popover-content {
display: flex;
flex-direction: row;
gap: 0.25rem;
border-radius: 0.5rem;
background: var(--island-bg-color);
box-shadow: var(--shadow-island);
padding: 0.5rem;
z-index: var(--zIndex-layerUI);
}
&:focus {
outline: none;
}
}

View File

@@ -0,0 +1,120 @@
import React, { useEffect, useState } from "react";
import clsx from "clsx";
import { capitalizeString } from "@excalidraw/common";
import * as Popover from "@radix-ui/react-popover";
import { trackEvent } from "../analytics";
import { ToolButton } from "./ToolButton";
import "./ToolPopover.scss";
import type { AppClassProperties } from "../types";
type ToolOption = {
type: string;
icon: React.ReactNode;
title?: string;
};
type ToolPopoverProps = {
app: AppClassProperties;
options: readonly ToolOption[];
activeTool: { type: string };
defaultOption: string;
className?: string;
namePrefix: string;
title: string;
"data-testid": string;
onToolChange: (type: string) => void;
displayedOption: ToolOption;
fillable?: boolean;
};
export const ToolPopover = ({
app,
options,
activeTool,
defaultOption,
className = "Shape",
namePrefix,
title,
"data-testid": dataTestId,
onToolChange,
displayedOption,
fillable = false,
}: ToolPopoverProps) => {
const [isPopupOpen, setIsPopupOpen] = useState(false);
const currentType = activeTool.type;
const isActive = displayedOption.type === currentType;
const SIDE_OFFSET = 32 / 2 + 10;
// if currentType is not in options, close popup
if (!options.some((o) => o.type === currentType) && isPopupOpen) {
setIsPopupOpen(false);
}
// Close popover when user starts interacting with the canvas (pointer down)
useEffect(() => {
// app.onPointerDownEmitter emits when pointer down happens on canvas area
const unsubscribe = app.onPointerDownEmitter.on(() => {
setIsPopupOpen(false);
});
return () => unsubscribe?.();
}, [app]);
return (
<Popover.Root open={isPopupOpen}>
<Popover.Trigger asChild>
<ToolButton
className={clsx(className, {
fillable,
active: options.some((o) => o.type === activeTool.type),
})}
type="radio"
icon={displayedOption.icon}
checked={isActive}
name="editor-current-shape"
title={title}
aria-label={title}
data-testid={dataTestId}
onPointerDown={() => {
setIsPopupOpen((v) => !v);
onToolChange(defaultOption);
}}
/>
</Popover.Trigger>
<Popover.Content
className="tool-popover-content"
sideOffset={SIDE_OFFSET}
>
{options.map(({ type, icon, title }) => (
<ToolButton
className={clsx(className, {
active: currentType === type,
})}
key={type}
type="radio"
icon={icon}
checked={currentType === type}
name={`${namePrefix}-option`}
title={title || capitalizeString(type)}
keyBindingLabel=""
aria-label={title || capitalizeString(type)}
data-testid={`toolbar-${type}`}
onChange={() => {
if (app.state.activeTool.type !== type) {
trackEvent("toolbar", type, "ui");
}
app.setActiveTool({ type: type as any });
onToolChange?.(type);
}}
/>
))}
</Popover.Content>
</Popover.Root>
);
};

View File

@@ -44,6 +44,10 @@
var(--button-active-border, var(--color-primary-darkest)) inset; var(--button-active-border, var(--color-primary-darkest)) inset;
} }
&:hover {
background-color: transparent;
}
&--selected, &--selected,
&--selected:hover { &--selected:hover {
background: var(--color-primary-light); background: var(--color-primary-light);

View File

@@ -3,24 +3,46 @@
.excalidraw { .excalidraw {
.dropdown-menu { .dropdown-menu {
position: absolute; position: absolute;
top: 100%; top: 2.5rem;
margin-top: 0.5rem; margin-top: 0.5rem;
&--placement-top {
top: auto;
bottom: 100%;
margin-top: 0;
margin-bottom: 0.5rem;
}
&--mobile { &--mobile {
left: 0;
width: 100%; width: 100%;
row-gap: 0.75rem; row-gap: 0.75rem;
// When main menu is in the top toolbar, position relative to trigger
&.main-menu-dropdown {
min-width: 232px;
max-width: calc(100vw - var(--editor-container-padding) * 2);
margin-top: 0;
margin-bottom: 0;
z-index: var(--zIndex-layerUI);
@media screen and (orientation: landscape) {
max-width: 232px;
}
}
.dropdown-menu-container { .dropdown-menu-container {
padding: 8px 8px; padding: 8px 8px;
box-sizing: border-box; box-sizing: border-box;
// background-color: var(--island-bg-color); max-height: calc(
100svh - var(--editor-container-padding) * 2 - 2.25rem
);
box-shadow: var(--shadow-island); box-shadow: var(--shadow-island);
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
position: relative; position: relative;
transition: box-shadow 0.5s ease-in-out; transition: box-shadow 0.5s ease-in-out;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto;
&.zen-mode { &.zen-mode {
box-shadow: none; box-shadow: none;
@@ -30,7 +52,7 @@
.dropdown-menu-container { .dropdown-menu-container {
background-color: var(--island-bg-color); background-color: var(--island-bg-color);
max-height: calc(100vh - 150px);
overflow-y: auto; overflow-y: auto;
--gap: 2; --gap: 2;
} }

View File

@@ -17,16 +17,27 @@ import "./DropdownMenu.scss";
const DropdownMenu = ({ const DropdownMenu = ({
children, children,
open, open,
placement,
}: { }: {
children?: React.ReactNode; children?: React.ReactNode;
open: boolean; open: boolean;
placement?: "top" | "bottom";
}) => { }) => {
const MenuTriggerComp = getMenuTriggerComponent(children); const MenuTriggerComp = getMenuTriggerComponent(children);
const MenuContentComp = getMenuContentComponent(children); const MenuContentComp = getMenuContentComponent(children);
// clone the MenuContentComp to pass the placement prop
const MenuContentCompWithPlacement =
MenuContentComp && React.isValidElement(MenuContentComp)
? React.cloneElement(MenuContentComp as React.ReactElement<any>, {
placement,
})
: MenuContentComp;
return ( return (
<> <>
{MenuTriggerComp} {MenuTriggerComp}
{open && MenuContentComp} {open && MenuContentCompWithPlacement}
</> </>
); );
}; };

View File

@@ -17,6 +17,7 @@ const MenuContent = ({
className = "", className = "",
onSelect, onSelect,
style, style,
placement = "bottom",
}: { }: {
children?: React.ReactNode; children?: React.ReactNode;
onClickOutside?: () => void; onClickOutside?: () => void;
@@ -26,6 +27,7 @@ const MenuContent = ({
*/ */
onSelect?: (event: Event) => void; onSelect?: (event: Event) => void;
style?: React.CSSProperties; style?: React.CSSProperties;
placement?: "top" | "bottom";
}) => { }) => {
const device = useDevice(); const device = useDevice();
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
@@ -58,6 +60,7 @@ const MenuContent = ({
const classNames = clsx(`dropdown-menu ${className}`, { const classNames = clsx(`dropdown-menu ${className}`, {
"dropdown-menu--mobile": device.editor.isMobile, "dropdown-menu--mobile": device.editor.isMobile,
"dropdown-menu--placement-top": placement === "top",
}).trim(); }).trim();
return ( return (

View File

@@ -2319,22 +2319,10 @@ export const adjustmentsIcon = createIcon(
tablerIconProps, tablerIconProps,
); );
export const backgroundIcon = createIcon(
<g strokeWidth={1}>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6 10l4 -4" />
<path d="M6 14l8 -8" />
<path d="M6 18l12 -12" />
<path d="M10 18l8 -8" />
<path d="M14 18l4 -4" />
</g>,
tablerIconProps,
);
export const strokeIcon = createIcon( export const strokeIcon = createIcon(
<g strokeWidth={1}> <g strokeWidth={1}>
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="6" width="12" height="12" fill="none" /> <path d="M6 10l4 -4 L6 14l8 -8 L6 18l12 -12 L10 18l8 -8 L14 18l4 -4" />
</g>, </g>,
tablerIconProps, tablerIconProps,
); );

View File

@@ -53,6 +53,8 @@ const MainMenu = Object.assign(
onSelect={composeEventHandlers(onSelect, () => { onSelect={composeEventHandlers(onSelect, () => {
setAppState({ openMenu: null }); setAppState({ openMenu: null });
})} })}
placement="bottom"
className={device.editor.isMobile ? "main-menu-dropdown" : ""}
> >
{children} {children}
{device.editor.isMobile && appState.collaborators.size > 0 && ( {device.editor.isMobile && appState.collaborators.size > 0 && (

View File

@@ -89,7 +89,7 @@ export const SHAPES = [
] as const; ] as const;
export const getToolbarTools = (app: AppClassProperties) => { export const getToolbarTools = (app: AppClassProperties) => {
return app.defaultSelectionTool === "lasso" return app.state.preferredSelectionTool.type === "lasso"
? ([ ? ([
{ {
value: "lasso", value: "lasso",

View File

@@ -252,16 +252,12 @@
} }
} }
@media (max-height: 599px) { &.excalidraw--mobile {
.welcome-screen-center { .welcome-screen-center {
margin-top: 4rem; margin-bottom: 2rem;
}
}
@media (min-height: 600px) and (max-height: 900px) {
.welcome-screen-center {
margin-top: 8rem;
} }
} }
@media (max-height: 500px), (max-width: 320px) { @media (max-height: 500px), (max-width: 320px) {
.welcome-screen-center { .welcome-screen-center {
display: none; display: none;

View File

@@ -44,6 +44,11 @@ body.excalidraw-cursor-resize * {
height: 100%; height: 100%;
width: 100%; width: 100%;
button,
label {
@include buttonNoHighlight;
}
button { button {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
@@ -235,27 +240,32 @@ body.excalidraw-cursor-resize * {
z-index: var(--zIndex-layerUI); z-index: var(--zIndex-layerUI);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; }
.App-welcome-screen {
z-index: var(--zIndex-layerUI);
} }
.App-bottom-bar { .App-bottom-bar {
position: absolute; position: absolute;
top: 0; // account for margins
width: calc(100% - 28px);
max-width: 450px;
bottom: 0; bottom: 0;
left: 0; left: 50%;
right: 0; transform: translateX(-50%);
--bar-padding: calc(4 * var(--space-factor)); --bar-padding: calc(4 * var(--space-factor));
z-index: 4; z-index: var(--zIndex-layerUI);
display: flex; display: flex;
align-items: flex-end; flex-direction: column;
pointer-events: none; pointer-events: none;
justify-content: center;
> .Island { > .Island {
width: 100%;
max-width: 100%;
min-width: 100%;
box-sizing: border-box; box-sizing: border-box;
max-height: 100%; max-height: 100%;
padding: 4px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
pointer-events: var(--ui-pointerEvents); pointer-events: var(--ui-pointerEvents);
@@ -263,7 +273,8 @@ body.excalidraw-cursor-resize * {
} }
.App-toolbar { .App-toolbar {
width: 100%; display: flex;
justify-content: center;
.eraser { .eraser {
&.ToolIcon:hover { &.ToolIcon:hover {
@@ -276,16 +287,15 @@ body.excalidraw-cursor-resize * {
} }
} }
.App-toolbar-content { .excalidraw-ui-top-left {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 0.5rem;
padding: 8px; }
.dropdown-menu--mobile { .App-toolbar-content {
bottom: 55px; display: flex;
top: auto; flex-direction: column;
}
} }
.App-mobile-menu { .App-mobile-menu {
@@ -506,7 +516,7 @@ body.excalidraw-cursor-resize * {
display: none; display: none;
} }
.scroll-back-to-content { .scroll-back-to-content {
bottom: calc(80px + var(--sab, 0)); bottom: calc(100px + var(--sab, 0));
z-index: -1; z-index: -1;
} }
} }

View File

@@ -8,6 +8,8 @@
--button-gray-1: #{$oc-gray-2}; --button-gray-1: #{$oc-gray-2};
--button-gray-2: #{$oc-gray-4}; --button-gray-2: #{$oc-gray-4};
--button-gray-3: #{$oc-gray-5}; --button-gray-3: #{$oc-gray-5};
--mobile-action-button-bg: rgba(255, 255, 255, 0.35);
--mobile-color-border: var(--default-border-color);
--button-special-active-bg-color: #{$oc-green-0}; --button-special-active-bg-color: #{$oc-green-0};
--dialog-border-color: var(--color-gray-20); --dialog-border-color: var(--color-gray-20);
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>'); --dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
@@ -42,6 +44,11 @@
--lg-button-size: 2.25rem; --lg-button-size: 2.25rem;
--lg-icon-size: 1rem; --lg-icon-size: 1rem;
--editor-container-padding: 1rem; --editor-container-padding: 1rem;
--mobile-action-button-size: 2rem;
@include isMobile {
--editor-container-padding: 0.75rem;
}
@media screen and (min-device-width: 1921px) { @media screen and (min-device-width: 1921px) {
--lg-button-size: 2.5rem; --lg-button-size: 2.5rem;
@@ -177,6 +184,8 @@
--button-gray-1: #363636; --button-gray-1: #363636;
--button-gray-2: #272727; --button-gray-2: #272727;
--button-gray-3: #222; --button-gray-3: #222;
--mobile-action-button-bg: var(--island-bg-color);
--mobile-color-border: rgba(255, 255, 255, 0.85);
--button-special-active-bg-color: #204624; --button-special-active-bg-color: #204624;
--dialog-border-color: var(--color-gray-80); --dialog-border-color: var(--color-gray-80);
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>'); --dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');

View File

@@ -122,6 +122,17 @@
color: var(--button-color, var(--color-on-primary-container)); color: var(--button-color, var(--color-on-primary-container));
} }
} }
@include isMobile() {
width: var(--mobile-action-button-size, var(--default-button-size));
height: var(--mobile-action-button-size, var(--default-button-size));
}
}
@mixin buttonNoHighlight {
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
user-select: none;
} }
@mixin outlineButtonIconStyles { @mixin outlineButtonIconStyles {
@@ -187,4 +198,9 @@
&:active { &:active {
box-shadow: 0 0 0 1px var(--color-brand-active); box-shadow: 0 0 0 1px var(--color-brand-active);
} }
@include isMobile() {
width: var(--mobile-action-button-size, 2rem);
height: var(--mobile-action-button-size, 2rem);
}
} }

View File

@@ -28,6 +28,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
excalidrawAPI, excalidrawAPI,
isCollaborating = false, isCollaborating = false,
onPointerUpdate, onPointerUpdate,
renderTopLeftUI,
renderTopRightUI, renderTopRightUI,
langCode = defaultLang.code, langCode = defaultLang.code,
viewModeEnabled, viewModeEnabled,
@@ -120,6 +121,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
excalidrawAPI={excalidrawAPI} excalidrawAPI={excalidrawAPI}
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
onPointerUpdate={onPointerUpdate} onPointerUpdate={onPointerUpdate}
renderTopLeftUI={renderTopLeftUI}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
langCode={langCode} langCode={langCode}
viewModeEnabled={viewModeEnabled} viewModeEnabled={viewModeEnabled}

View File

@@ -957,6 +957,10 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -1153,6 +1157,10 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -1367,6 +1375,10 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -1698,6 +1710,10 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -2029,6 +2045,10 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -2243,6 +2263,10 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -2484,6 +2508,10 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -2782,6 +2810,10 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id3": true, "id3": true,
}, },
@@ -3154,6 +3186,10 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -3647,6 +3683,10 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -3970,6 +4010,10 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -4293,6 +4337,10 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id3": true, "id3": true,
}, },
@@ -5578,6 +5626,10 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -6795,6 +6847,10 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -7733,6 +7789,10 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -8730,6 +8790,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -9724,6 +9788,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,

View File

@@ -79,6 +79,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id4": true, "id4": true,
}, },
@@ -712,6 +716,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id4": true, "id4": true,
}, },
@@ -1276,6 +1284,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -1636,6 +1648,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -1998,6 +2014,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -2258,6 +2278,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -2714,6 +2738,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -3017,6 +3045,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -3336,6 +3368,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -3630,6 +3666,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -3916,6 +3956,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -4151,6 +4195,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -4408,6 +4456,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -4679,6 +4731,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -4908,6 +4964,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -5137,6 +5197,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -5384,6 +5448,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -5640,6 +5708,10 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -5895,6 +5967,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id1": true, "id1": true,
}, },
@@ -6215,7 +6291,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"offsetTop": 0, "offsetTop": 0,
"openDialog": null, "openDialog": null,
"openMenu": null, "openMenu": null,
"openPopup": "elementBackground", "openPopup": null,
"openSidebar": null, "openSidebar": null,
"originSnapOffset": null, "originSnapOffset": null,
"pasteDialog": { "pasteDialog": {
@@ -6224,6 +6300,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id8": true, "id8": true,
}, },
@@ -6651,6 +6731,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id1": true, "id1": true,
}, },
@@ -7028,6 +7112,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -7337,6 +7425,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -7629,6 +7721,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -7859,6 +7955,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -8211,6 +8311,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -8563,6 +8667,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id3": true, "id3": true,
@@ -8969,6 +9077,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -9248,6 +9360,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -9512,6 +9628,10 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -9777,6 +9897,10 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -10012,6 +10136,10 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -10306,6 +10434,10 @@ exports[`history > multiplayer undo/redo > should override remotely added points
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -10623,6 +10755,10 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -10862,6 +10998,10 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -11301,6 +11441,10 @@ exports[`history > multiplayer undo/redo > should update history entries after r
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -11561,6 +11705,10 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -11796,6 +11944,10 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -12024,7 +12176,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"offsetTop": 0, "offsetTop": 0,
"openDialog": null, "openDialog": null,
"openMenu": null, "openMenu": null,
"openPopup": "elementStroke", "openPopup": null,
"openSidebar": null, "openSidebar": null,
"originSnapOffset": null, "originSnapOffset": null,
"pasteDialog": { "pasteDialog": {
@@ -12033,6 +12185,10 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -12427,6 +12583,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -12634,6 +12794,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -12844,6 +13008,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -13142,6 +13310,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -13443,6 +13615,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": -50, "scrollX": -50,
@@ -13685,6 +13861,10 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -13922,6 +14102,10 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -14159,6 +14343,10 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -14406,6 +14594,10 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -14740,6 +14932,10 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -14907,6 +15103,10 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -15194,6 +15394,10 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -15457,6 +15661,10 @@ exports[`history > singleplayer undo/redo > should not modify anything on unrela
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -15598,7 +15806,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
"offsetTop": 0, "offsetTop": 0,
"openDialog": null, "openDialog": null,
"openMenu": null, "openMenu": null,
"openPopup": "elementBackground", "openPopup": null,
"openSidebar": null, "openSidebar": null,
"originSnapOffset": null, "originSnapOffset": null,
"pasteDialog": { "pasteDialog": {
@@ -15607,6 +15815,10 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -15892,6 +16104,10 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -16051,6 +16267,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -16799,6 +17019,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -17445,6 +17669,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -18091,6 +18319,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -18840,6 +19072,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -19608,6 +19844,10 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -20088,6 +20328,10 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id1": true, "id1": true,
}, },
@@ -20599,6 +20843,10 @@ exports[`history > singleplayer undo/redo > should support element creation, del
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id3": true, "id3": true,
}, },
@@ -21058,6 +21306,10 @@ exports[`history > singleplayer undo/redo > should support linear element creati
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },

View File

@@ -80,6 +80,10 @@ exports[`given element A and group of elements B and given both are selected whe
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id3": true, "id3": true,
@@ -506,6 +510,10 @@ exports[`given element A and group of elements B and given both are selected whe
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id3": true, "id3": true,
@@ -922,6 +930,10 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -1488,6 +1500,10 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -1695,6 +1711,10 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -2079,6 +2099,10 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -2324,6 +2348,10 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -2504,6 +2532,10 @@ exports[`regression tests > can drag element that covers another element, while
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id6": true, "id6": true,
}, },
@@ -2829,6 +2861,10 @@ exports[`regression tests > change the properties of a shape > [end of test] app
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -3084,6 +3120,10 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -3325,6 +3365,10 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -3561,6 +3605,10 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id3": true, "id3": true,
}, },
@@ -3819,6 +3867,10 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id6": true, "id6": true,
}, },
@@ -4133,6 +4185,10 @@ exports[`regression tests > deleting last but one element in editing group shoul
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -4569,6 +4625,10 @@ exports[`regression tests > deselects group of selected elements on pointer down
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id3": true, "id3": true,
@@ -4852,6 +4912,10 @@ exports[`regression tests > deselects group of selected elements on pointer up w
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id3": true, "id3": true,
@@ -5128,6 +5192,10 @@ exports[`regression tests > deselects selected element on pointer down when poin
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -5336,6 +5404,10 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -5536,6 +5608,10 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -5929,6 +6005,10 @@ exports[`regression tests > drags selected elements from point inside common bou
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id3": true, "id3": true,
@@ -6226,6 +6306,10 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -7036,6 +7120,10 @@ exports[`regression tests > given a group of selected elements with an element t
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id6": true, "id6": true,
@@ -7361,7 +7449,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
"offsetTop": 0, "offsetTop": 0,
"openDialog": null, "openDialog": null,
"openMenu": null, "openMenu": null,
"openPopup": "elementBackground", "openPopup": null,
"openSidebar": null, "openSidebar": null,
"originSnapOffset": null, "originSnapOffset": null,
"pasteDialog": { "pasteDialog": {
@@ -7370,6 +7458,10 @@ exports[`regression tests > given a selected element A and a not selected elemen
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -7649,6 +7741,10 @@ exports[`regression tests > given selected element A with lower z-index than uns
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -7884,6 +7980,10 @@ exports[`regression tests > given selected element A with lower z-index than uns
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -8124,6 +8224,10 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -8304,6 +8408,10 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -8484,6 +8592,10 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -8664,6 +8776,10 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -8893,6 +9009,10 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -9120,6 +9240,10 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -9312,6 +9436,10 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -9541,6 +9669,10 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -9721,6 +9853,10 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -9948,6 +10084,10 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -10128,6 +10268,10 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -10320,6 +10464,10 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -10500,6 +10648,10 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id3": true, "id3": true,
@@ -11031,6 +11183,10 @@ exports[`regression tests > noop interaction after undo shouldn't create history
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -11311,6 +11467,10 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": "-6.25000", "scrollX": "-6.25000",
@@ -11434,6 +11594,10 @@ exports[`regression tests > shift click on selected element should deselect it o
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -11634,6 +11798,10 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id3": true, "id3": true,
@@ -11953,6 +12121,10 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id3": true, "id3": true,
@@ -12382,6 +12554,10 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
"id15": true, "id15": true,
@@ -13025,6 +13201,10 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 60, "scrollX": 60,
@@ -13148,6 +13328,10 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id0": true, "id0": true,
}, },
@@ -13779,6 +13963,10 @@ exports[`regression tests > switches from group of selected elements to another
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id3": true, "id3": true,
"id6": true, "id6": true,
@@ -14118,6 +14306,10 @@ exports[`regression tests > switches selected element on pointer down > [end of
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": { "previousSelectedElementIds": {
"id3": true, "id3": true,
}, },
@@ -14382,6 +14574,10 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 20, "scrollX": 20,
@@ -14505,6 +14701,10 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -14869,6 +15069,10 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
@@ -14995,6 +15199,10 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": true,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,

View File

@@ -322,6 +322,10 @@ export interface AppState {
// indicates if the current tool is temporarily switched on from the selection tool // indicates if the current tool is temporarily switched on from the selection tool
fromSelection: boolean; fromSelection: boolean;
} & ActiveTool; } & ActiveTool;
preferredSelectionTool: {
type: "selection" | "lasso";
initialized: boolean;
};
penMode: boolean; penMode: boolean;
penDetected: boolean; penDetected: boolean;
exportBackground: boolean; exportBackground: boolean;
@@ -370,7 +374,6 @@ export interface AppState {
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" } | { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
| { name: "commandPalette" } | { name: "commandPalette" }
| { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] }; | { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] };
/** /**
* Reflects user preference for whether the default sidebar should be docked. * Reflects user preference for whether the default sidebar should be docked.
* *
@@ -455,7 +458,7 @@ export interface AppState {
bindMode: BindMode; bindMode: BindMode;
/** properties sidebar mode - determines whether to show compact or complete sidebar */ /** properties sidebar mode - determines whether to show compact or complete sidebar */
stylesPanelMode: "compact" | "full"; stylesPanelMode: "compact" | "full" | "mobile";
} }
export type SearchMatch = { export type SearchMatch = {
@@ -578,6 +581,10 @@ export interface ExcalidrawProps {
/** excludes the duplicated elements */ /** excludes the duplicated elements */
prevElements: readonly ExcalidrawElement[], prevElements: readonly ExcalidrawElement[],
) => ExcalidrawElement[] | void; ) => ExcalidrawElement[] | void;
renderTopLeftUI?: (
isMobile: boolean,
appState: UIAppState,
) => JSX.Element | null;
renderTopRightUI?: ( renderTopRightUI?: (
isMobile: boolean, isMobile: boolean,
appState: UIAppState, appState: UIAppState,
@@ -745,8 +752,7 @@ export type AppClassProperties = {
onPointerUpEmitter: App["onPointerUpEmitter"]; onPointerUpEmitter: App["onPointerUpEmitter"];
updateEditorAtom: App["updateEditorAtom"]; updateEditorAtom: App["updateEditorAtom"];
onPointerDownEmitter: App["onPointerDownEmitter"];
defaultSelectionTool: "selection" | "lasso";
bindModeHandler: App["bindModeHandler"]; bindModeHandler: App["bindModeHandler"];
}; };

View File

@@ -226,22 +226,6 @@ export const textWysiwyg = ({
} }
} }
const [viewportX, viewportY] = getViewportCoords(coordX, coordY); const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
const initialSelectionStart = editable.selectionStart;
const initialSelectionEnd = editable.selectionEnd;
const initialLength = editable.value.length;
// restore cursor position after value updated so it doesn't
// go to the end of text when container auto expanded
if (
initialSelectionStart === initialSelectionEnd &&
initialSelectionEnd !== initialLength
) {
// get diff between length and selection end and shift
// the cursor by "diff" times to position correctly
const diff = initialLength - initialSelectionEnd;
editable.selectionStart = editable.value.length - diff;
editable.selectionEnd = editable.value.length - diff;
}
if (!container) { if (!container) {
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value; maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;

View File

@@ -81,6 +81,10 @@ exports[`exportToSvg > with default arguments 1`] = `
}, },
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"preferredSelectionTool": {
"initialized": false,
"type": "selection",
},
"previousSelectedElementIds": {}, "previousSelectedElementIds": {},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,