feat: canvas search (#8438)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
5a11c70714
commit
6959a363f0
@ -8,3 +8,4 @@ public/workbox
|
|||||||
packages/excalidraw/types
|
packages/excalidraw/types
|
||||||
examples/**/public
|
examples/**/public
|
||||||
dev-dist
|
dev-dist
|
||||||
|
coverage
|
||||||
|
@ -31,6 +31,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MainMenu.DefaultItems.CommandPalette className="highlighted" />
|
<MainMenu.DefaultItems.CommandPalette className="highlighted" />
|
||||||
|
<MainMenu.DefaultItems.SearchMenu />
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
|
51
packages/excalidraw/actions/actionToggleSearchMenu.ts
Normal file
51
packages/excalidraw/actions/actionToggleSearchMenu.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { KEYS } from "../keys";
|
||||||
|
import { register } from "./register";
|
||||||
|
import type { AppState } from "../types";
|
||||||
|
import { searchIcon } from "../components/icons";
|
||||||
|
import { StoreAction } from "../store";
|
||||||
|
import { CLASSES, SEARCH_SIDEBAR } from "../constants";
|
||||||
|
|
||||||
|
export const actionToggleSearchMenu = register({
|
||||||
|
name: "searchMenu",
|
||||||
|
icon: searchIcon,
|
||||||
|
keywords: ["search", "find"],
|
||||||
|
label: "search.title",
|
||||||
|
viewMode: true,
|
||||||
|
trackEvent: {
|
||||||
|
category: "search_menu",
|
||||||
|
action: "toggle",
|
||||||
|
predicate: (appState) => appState.gridModeEnabled,
|
||||||
|
},
|
||||||
|
perform(elements, appState, _, app) {
|
||||||
|
if (appState.openSidebar?.name === SEARCH_SIDEBAR.name) {
|
||||||
|
const searchInput =
|
||||||
|
app.excalidrawContainerValue.container?.querySelector<HTMLInputElement>(
|
||||||
|
`.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (searchInput?.matches(":focus")) {
|
||||||
|
return {
|
||||||
|
appState: { ...appState, openSidebar: null },
|
||||||
|
storeAction: StoreAction.NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
searchInput?.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
openSidebar: { name: SEARCH_SIDEBAR.name },
|
||||||
|
openDialog: null,
|
||||||
|
},
|
||||||
|
storeAction: StoreAction.NONE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
checked: (appState: AppState) => appState.gridModeEnabled,
|
||||||
|
predicate: (element, appState, props) => {
|
||||||
|
return props.gridModeEnabled === undefined;
|
||||||
|
},
|
||||||
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.F,
|
||||||
|
});
|
@ -86,3 +86,5 @@ export { actionUnbindText, actionBindText } from "./actionBoundText";
|
|||||||
export { actionLink } from "./actionLink";
|
export { actionLink } from "./actionLink";
|
||||||
export { actionToggleElementLock } from "./actionElementLock";
|
export { actionToggleElementLock } from "./actionElementLock";
|
||||||
export { actionToggleLinearEditor } from "./actionLinearEditor";
|
export { actionToggleLinearEditor } from "./actionLinearEditor";
|
||||||
|
|
||||||
|
export { actionToggleSearchMenu } from "./actionToggleSearchMenu";
|
||||||
|
@ -51,7 +51,8 @@ export type ShortcutName =
|
|||||||
>
|
>
|
||||||
| "saveScene"
|
| "saveScene"
|
||||||
| "imageExport"
|
| "imageExport"
|
||||||
| "commandPalette";
|
| "commandPalette"
|
||||||
|
| "searchMenu";
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
||||||
@ -112,6 +113,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
saveFileToDisk: [getShortcutKey("CtrlOrCmd+S")],
|
saveFileToDisk: [getShortcutKey("CtrlOrCmd+S")],
|
||||||
saveToActiveFile: [getShortcutKey("CtrlOrCmd+S")],
|
saveToActiveFile: [getShortcutKey("CtrlOrCmd+S")],
|
||||||
toggleShortcuts: [getShortcutKey("?")],
|
toggleShortcuts: [getShortcutKey("?")],
|
||||||
|
searchMenu: [getShortcutKey("CtrlOrCmd+F")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (name: ShortcutName, idx = 0) => {
|
export const getShortcutFromShortcutName = (name: ShortcutName, idx = 0) => {
|
||||||
|
@ -137,7 +137,8 @@ export type ActionName =
|
|||||||
| "wrapTextInContainer"
|
| "wrapTextInContainer"
|
||||||
| "commandPalette"
|
| "commandPalette"
|
||||||
| "autoResize"
|
| "autoResize"
|
||||||
| "elementStats";
|
| "elementStats"
|
||||||
|
| "searchMenu";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
@ -191,7 +192,8 @@ export interface Action {
|
|||||||
| "history"
|
| "history"
|
||||||
| "menu"
|
| "menu"
|
||||||
| "collab"
|
| "collab"
|
||||||
| "hyperlink";
|
| "hyperlink"
|
||||||
|
| "search_menu";
|
||||||
action?: string;
|
action?: string;
|
||||||
predicate?: (
|
predicate?: (
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
@ -116,6 +116,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
objectsSnapModeEnabled: false,
|
objectsSnapModeEnabled: false,
|
||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
followedBy: new Set(),
|
followedBy: new Set(),
|
||||||
|
searchMatches: [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -236,6 +237,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
||||||
userToFollow: { browser: false, export: false, server: false },
|
userToFollow: { browser: false, export: false, server: false },
|
||||||
followedBy: { browser: false, export: false, server: false },
|
followedBy: { browser: false, export: false, server: false },
|
||||||
|
searchMatches: { browser: false, export: false, server: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <
|
const _clearAppStateForStorage = <
|
||||||
|
@ -440,6 +440,7 @@ import {
|
|||||||
FlowChartNavigator,
|
FlowChartNavigator,
|
||||||
getLinkDirectionFromKey,
|
getLinkDirectionFromKey,
|
||||||
} from "../element/flowchart";
|
} from "../element/flowchart";
|
||||||
|
import { searchItemInFocusAtom } from "./SearchMenu";
|
||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
import { point, pointDistance, vector } from "../../math";
|
import { point, pointDistance, vector } from "../../math";
|
||||||
|
|
||||||
@ -548,6 +549,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
public scene: Scene;
|
public scene: Scene;
|
||||||
public fonts: Fonts;
|
public fonts: Fonts;
|
||||||
public renderer: Renderer;
|
public renderer: Renderer;
|
||||||
|
public visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
private resizeObserver: ResizeObserver | undefined;
|
private resizeObserver: ResizeObserver | undefined;
|
||||||
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
||||||
public library: AppClassProperties["library"];
|
public library: AppClassProperties["library"];
|
||||||
@ -555,7 +557,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
public id: string;
|
public id: string;
|
||||||
private store: Store;
|
private store: Store;
|
||||||
private history: History;
|
private history: History;
|
||||||
private excalidrawContainerValue: {
|
public excalidrawContainerValue: {
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@ -682,6 +684,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.canvas = document.createElement("canvas");
|
this.canvas = document.createElement("canvas");
|
||||||
this.rc = rough.canvas(this.canvas);
|
this.rc = rough.canvas(this.canvas);
|
||||||
this.renderer = new Renderer(this.scene);
|
this.renderer = new Renderer(this.scene);
|
||||||
|
this.visibleElements = [];
|
||||||
|
|
||||||
this.store = new Store();
|
this.store = new Store();
|
||||||
this.history = new History();
|
this.history = new History();
|
||||||
@ -1480,6 +1483,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
newElementId: this.state.newElement?.id,
|
newElementId: this.state.newElement?.id,
|
||||||
pendingImageElementId: this.state.pendingImageElementId,
|
pendingImageElementId: this.state.pendingImageElementId,
|
||||||
});
|
});
|
||||||
|
this.visibleElements = visibleElements;
|
||||||
|
|
||||||
const allElementsMap = this.scene.getNonDeletedElementsMap();
|
const allElementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
@ -3800,7 +3804,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
private getEditorUIOffsets = (): {
|
public getEditorUIOffsets = (): {
|
||||||
top: number;
|
top: number;
|
||||||
right: number;
|
right: number;
|
||||||
bottom: number;
|
bottom: number;
|
||||||
@ -5973,6 +5977,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
|
this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
|
||||||
this.maybeUnfollowRemoteUser();
|
this.maybeUnfollowRemoteUser();
|
||||||
|
|
||||||
|
if (this.state.searchMatches) {
|
||||||
|
this.setState((state) => ({
|
||||||
|
searchMatches: state.searchMatches.map((searchMatch) => ({
|
||||||
|
...searchMatch,
|
||||||
|
focus: false,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
jotaiStore.set(searchItemInFocusAtom, null);
|
||||||
|
}
|
||||||
|
|
||||||
// since contextMenu options are potentially evaluated on each render,
|
// since contextMenu options are potentially evaluated on each render,
|
||||||
// and an contextMenu action may depend on selection state, we must
|
// and an contextMenu action may depend on selection state, we must
|
||||||
// close the contextMenu before we update the selection on pointerDown
|
// close the contextMenu before we update the selection on pointerDown
|
||||||
@ -6401,8 +6415,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
isPanning = true;
|
isPanning = true;
|
||||||
|
|
||||||
|
// due to event.preventDefault below, container wouldn't get focus
|
||||||
|
// automatically
|
||||||
|
this.focusContainer();
|
||||||
|
|
||||||
|
// preventing defualt while text editing messes with cursor/focus
|
||||||
if (!this.state.editingTextElement) {
|
if (!this.state.editingTextElement) {
|
||||||
// preventing defualt while text editing messes with cursor/focus
|
// necessary to prevent browser from scrolling the page if excalidraw
|
||||||
|
// not full-page #4489
|
||||||
|
//
|
||||||
|
// as such, the above is broken when panning canvas while in wysiwyg
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,11 @@ import { InlineIcon } from "../InlineIcon";
|
|||||||
import { SHAPES } from "../../shapes";
|
import { SHAPES } from "../../shapes";
|
||||||
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
|
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
|
||||||
import { useStableCallback } from "../../hooks/useStableCallback";
|
import { useStableCallback } from "../../hooks/useStableCallback";
|
||||||
import { actionClearCanvas, actionLink } from "../../actions";
|
import {
|
||||||
|
actionClearCanvas,
|
||||||
|
actionLink,
|
||||||
|
actionToggleSearchMenu,
|
||||||
|
} from "../../actions";
|
||||||
import { jotaiStore } from "../../jotai";
|
import { jotaiStore } from "../../jotai";
|
||||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||||
import type { CommandPaletteItem } from "./types";
|
import type { CommandPaletteItem } from "./types";
|
||||||
@ -382,6 +386,15 @@ function CommandPaletteInner({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("search.title"),
|
||||||
|
category: DEFAULT_CATEGORIES.app,
|
||||||
|
icon: searchIcon,
|
||||||
|
viewMode: true,
|
||||||
|
perform: () => {
|
||||||
|
actionManager.executeAction(actionToggleSearchMenu);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t("labels.changeStroke"),
|
label: t("labels.changeStroke"),
|
||||||
keywords: ["color", "outline"],
|
keywords: ["color", "outline"],
|
||||||
|
@ -2,7 +2,6 @@ import clsx from "clsx";
|
|||||||
import { DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB } from "../constants";
|
import { DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB } from "../constants";
|
||||||
import { useTunnels } from "../context/tunnels";
|
import { useTunnels } from "../context/tunnels";
|
||||||
import { useUIAppState } from "../context/ui-appState";
|
import { useUIAppState } from "../context/ui-appState";
|
||||||
import { t } from "../i18n";
|
|
||||||
import type { MarkOptional, Merge } from "../utility-types";
|
import type { MarkOptional, Merge } from "../utility-types";
|
||||||
import { composeEventHandlers } from "../utils";
|
import { composeEventHandlers } from "../utils";
|
||||||
import { useExcalidrawSetAppState } from "./App";
|
import { useExcalidrawSetAppState } from "./App";
|
||||||
@ -10,6 +9,8 @@ import { withInternalFallback } from "./hoc/withInternalFallback";
|
|||||||
import { LibraryMenu } from "./LibraryMenu";
|
import { LibraryMenu } from "./LibraryMenu";
|
||||||
import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
|
import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
|
||||||
import { Sidebar } from "./Sidebar/Sidebar";
|
import { Sidebar } from "./Sidebar/Sidebar";
|
||||||
|
import "../components/dropdownMenu/DropdownMenu.scss";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
const DefaultSidebarTrigger = withInternalFallback(
|
const DefaultSidebarTrigger = withInternalFallback(
|
||||||
"DefaultSidebarTrigger",
|
"DefaultSidebarTrigger",
|
||||||
@ -68,8 +69,7 @@ export const DefaultSidebar = Object.assign(
|
|||||||
return (
|
return (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
{...rest}
|
{...rest}
|
||||||
name="default"
|
name={"default"}
|
||||||
key="default"
|
|
||||||
className={clsx("default-sidebar", className)}
|
className={clsx("default-sidebar", className)}
|
||||||
docked={docked ?? appState.defaultSidebarDockedPreference}
|
docked={docked ?? appState.defaultSidebarDockedPreference}
|
||||||
onDock={
|
onDock={
|
||||||
|
@ -288,6 +288,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("stats.fullTitle")}
|
label={t("stats.fullTitle")}
|
||||||
shortcuts={[getShortcutKey("Alt+/")]}
|
shortcuts={[getShortcutKey("Alt+/")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("search.title")}
|
||||||
|
shortcuts={[getShortcutFromShortcutName("searchMenu")]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("commandPalette.title")}
|
label={t("commandPalette.title")}
|
||||||
shortcuts={
|
shortcuts={
|
||||||
|
@ -13,6 +13,7 @@ import { isEraserActive } from "../appState";
|
|||||||
import "./HintViewer.scss";
|
import "./HintViewer.scss";
|
||||||
import { isNodeInFlowchart } from "../element/flowchart";
|
import { isNodeInFlowchart } from "../element/flowchart";
|
||||||
import { isGridModeEnabled } from "../snapping";
|
import { isGridModeEnabled } from "../snapping";
|
||||||
|
import { SEARCH_SIDEBAR } from "../constants";
|
||||||
|
|
||||||
interface HintViewerProps {
|
interface HintViewerProps {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
@ -30,6 +31,13 @@ const getHints = ({
|
|||||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||||
const multiMode = appState.multiElement !== null;
|
const multiMode = appState.multiElement !== null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
appState.openSidebar?.name === SEARCH_SIDEBAR.name &&
|
||||||
|
appState.searchMatches?.length
|
||||||
|
) {
|
||||||
|
return t("hints.dismissSearch");
|
||||||
|
}
|
||||||
|
|
||||||
if (appState.openSidebar && !device.editor.canFitSidebar) {
|
if (appState.openSidebar && !device.editor.canFitSidebar) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
CLASSES,
|
CLASSES,
|
||||||
DEFAULT_SIDEBAR,
|
DEFAULT_SIDEBAR,
|
||||||
LIBRARY_SIDEBAR_WIDTH,
|
LIBRARY_SIDEBAR_WIDTH,
|
||||||
|
SEARCH_SIDEBAR,
|
||||||
TOOL_TYPE,
|
TOOL_TYPE,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { showSelectedShapeActions } from "../element";
|
import { showSelectedShapeActions } from "../element";
|
||||||
@ -63,6 +64,7 @@ import { LaserPointerButton } from "./LaserPointerButton";
|
|||||||
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { actionToggleStats } from "../actions";
|
import { actionToggleStats } from "../actions";
|
||||||
|
import { SearchSidebar } from "./SearchSidebar";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@ -99,6 +101,7 @@ const DefaultMainMenu: React.FC<{
|
|||||||
{UIOptions.canvasActions.saveAsImage && (
|
{UIOptions.canvasActions.saveAsImage && (
|
||||||
<MainMenu.DefaultItems.SaveAsImage />
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
)}
|
)}
|
||||||
|
<MainMenu.DefaultItems.SearchMenu />
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
@ -362,16 +365,21 @@ const LayerUI = ({
|
|||||||
|
|
||||||
const renderSidebars = () => {
|
const renderSidebars = () => {
|
||||||
return (
|
return (
|
||||||
<DefaultSidebar
|
<>
|
||||||
__fallback
|
{appState.openSidebar?.name === SEARCH_SIDEBAR.name && (
|
||||||
onDock={(docked) => {
|
<SearchSidebar />
|
||||||
trackEvent(
|
)}
|
||||||
"sidebar",
|
<DefaultSidebar
|
||||||
`toggleDock (${docked ? "dock" : "undock"})`,
|
__fallback
|
||||||
`(${device.editor.isMobile ? "mobile" : "desktop"})`,
|
onDock={(docked) => {
|
||||||
);
|
trackEvent(
|
||||||
}}
|
"sidebar",
|
||||||
/>
|
`toggleDock (${docked ? "dock" : "undock"})`,
|
||||||
|
`(${device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
110
packages/excalidraw/components/SearchMenu.scss
Normal file
110
packages/excalidraw/components/SearchMenu.scss
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
@import "open-color/open-color";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.layer-ui__search {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__search-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
.ExcTextField {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExcTextField__input {
|
||||||
|
background-color: #f5f5f9;
|
||||||
|
@at-root .excalidraw.theme--dark#{&} {
|
||||||
|
background-color: #31303b;
|
||||||
|
}
|
||||||
|
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__search-count {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 8px 0 8px;
|
||||||
|
margin: 0 0.75rem 0.25rem 0.75rem;
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
.result-nav {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.result-nav-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
--button-border: transparent;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-surface-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__search-result-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__result-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 2rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
margin: 0 0.75rem;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
|
||||||
|
.text-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-text {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 48px;
|
||||||
|
line-height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-surface-high);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-surface-high);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
671
packages/excalidraw/components/SearchMenu.tsx
Normal file
671
packages/excalidraw/components/SearchMenu.tsx
Normal file
@ -0,0 +1,671 @@
|
|||||||
|
import { Fragment, memo, useEffect, useRef, useState } from "react";
|
||||||
|
import { collapseDownIcon, upIcon, searchIcon } from "./icons";
|
||||||
|
import { TextField } from "./TextField";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { useApp, useExcalidrawSetAppState } from "./App";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import type { AppClassProperties } from "../types";
|
||||||
|
import { isTextElement, newTextElement } from "../element";
|
||||||
|
import type { ExcalidrawTextElement } from "../element/types";
|
||||||
|
import { measureText } from "../element/textElement";
|
||||||
|
import { addEventListener, getFontString } from "../utils";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
|
import "./SearchMenu.scss";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { isElementCompletelyInViewport } from "../element/sizeHelpers";
|
||||||
|
import { randomInteger } from "../random";
|
||||||
|
import { CLASSES, EVENT } from "../constants";
|
||||||
|
import { useStable } from "../hooks/useStable";
|
||||||
|
|
||||||
|
const searchKeywordAtom = atom<string>("");
|
||||||
|
export const searchItemInFocusAtom = atom<number | null>(null);
|
||||||
|
|
||||||
|
const SEARCH_DEBOUNCE = 350;
|
||||||
|
|
||||||
|
type SearchMatchItem = {
|
||||||
|
textElement: ExcalidrawTextElement;
|
||||||
|
keyword: string;
|
||||||
|
index: number;
|
||||||
|
preview: {
|
||||||
|
indexInKeyword: number;
|
||||||
|
previewText: string;
|
||||||
|
moreBefore: boolean;
|
||||||
|
moreAfter: boolean;
|
||||||
|
};
|
||||||
|
matchedLines: {
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchMatches = {
|
||||||
|
nonce: number | null;
|
||||||
|
items: SearchMatchItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SearchMenu = () => {
|
||||||
|
const app = useApp();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [keyword, setKeyword] = useAtom(searchKeywordAtom, jotaiScope);
|
||||||
|
const [searchMatches, setSearchMatches] = useState<SearchMatches>({
|
||||||
|
nonce: null,
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
const searchedKeywordRef = useRef<string | null>();
|
||||||
|
const lastSceneNonceRef = useRef<number | undefined>();
|
||||||
|
|
||||||
|
const [focusIndex, setFocusIndex] = useAtom(
|
||||||
|
searchItemInFocusAtom,
|
||||||
|
jotaiScope,
|
||||||
|
);
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const trimmedKeyword = keyword.trim();
|
||||||
|
if (
|
||||||
|
trimmedKeyword !== searchedKeywordRef.current ||
|
||||||
|
app.scene.getSceneNonce() !== lastSceneNonceRef.current
|
||||||
|
) {
|
||||||
|
searchedKeywordRef.current = null;
|
||||||
|
handleSearch(trimmedKeyword, app, (matchItems, index) => {
|
||||||
|
setSearchMatches({
|
||||||
|
nonce: randomInteger(),
|
||||||
|
items: matchItems,
|
||||||
|
});
|
||||||
|
setFocusIndex(index);
|
||||||
|
searchedKeywordRef.current = trimmedKeyword;
|
||||||
|
lastSceneNonceRef.current = app.scene.getSceneNonce();
|
||||||
|
setAppState({
|
||||||
|
searchMatches: matchItems.map((searchMatch) => ({
|
||||||
|
id: searchMatch.textElement.id,
|
||||||
|
focus: false,
|
||||||
|
matchedLines: searchMatch.matchedLines,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
keyword,
|
||||||
|
elementsMap,
|
||||||
|
app,
|
||||||
|
setAppState,
|
||||||
|
setFocusIndex,
|
||||||
|
lastSceneNonceRef,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const goToNextItem = () => {
|
||||||
|
if (searchMatches.items.length > 0) {
|
||||||
|
setFocusIndex((focusIndex) => {
|
||||||
|
if (focusIndex === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (focusIndex + 1) % searchMatches.items.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPreviousItem = () => {
|
||||||
|
if (searchMatches.items.length > 0) {
|
||||||
|
setFocusIndex((focusIndex) => {
|
||||||
|
if (focusIndex === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return focusIndex - 1 < 0
|
||||||
|
? searchMatches.items.length - 1
|
||||||
|
: focusIndex - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchMatches.items.length > 0 && focusIndex !== null) {
|
||||||
|
const match = searchMatches.items[focusIndex];
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const matchAsElement = newTextElement({
|
||||||
|
text: match.keyword,
|
||||||
|
x: match.textElement.x + (match.matchedLines[0]?.offsetX ?? 0),
|
||||||
|
y: match.textElement.y + (match.matchedLines[0]?.offsetY ?? 0),
|
||||||
|
width: match.matchedLines[0]?.width,
|
||||||
|
height: match.matchedLines[0]?.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isElementCompletelyInViewport(
|
||||||
|
[matchAsElement],
|
||||||
|
app.canvas.width / window.devicePixelRatio,
|
||||||
|
app.canvas.height / window.devicePixelRatio,
|
||||||
|
{
|
||||||
|
offsetLeft: app.state.offsetLeft,
|
||||||
|
offsetTop: app.state.offsetTop,
|
||||||
|
scrollX: app.state.scrollX,
|
||||||
|
scrollY: app.state.scrollY,
|
||||||
|
zoom: app.state.zoom,
|
||||||
|
},
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
app.getEditorUIOffsets(),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
app.scrollToContent(matchAsElement, {
|
||||||
|
fitToContent: true,
|
||||||
|
animate: true,
|
||||||
|
duration: 300,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextMatches = searchMatches.items.map((match, index) => {
|
||||||
|
if (index === focusIndex) {
|
||||||
|
return {
|
||||||
|
id: match.textElement.id,
|
||||||
|
focus: true,
|
||||||
|
matchedLines: match.matchedLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: match.textElement.id,
|
||||||
|
focus: false,
|
||||||
|
matchedLines: match.matchedLines,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setAppState({
|
||||||
|
searchMatches: nextMatches,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [app, focusIndex, searchMatches, setAppState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setFocusIndex(null);
|
||||||
|
searchedKeywordRef.current = null;
|
||||||
|
lastSceneNonceRef.current = undefined;
|
||||||
|
setAppState({
|
||||||
|
searchMatches: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [setAppState, setFocusIndex]);
|
||||||
|
|
||||||
|
const stableState = useStable({
|
||||||
|
goToNextItem,
|
||||||
|
goToPreviousItem,
|
||||||
|
searchMatches,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const eventHandler = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
event.key === KEYS.ESCAPE &&
|
||||||
|
!app.state.openDialog &&
|
||||||
|
!app.state.openPopup
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setAppState({
|
||||||
|
openSidebar: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.F) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!searchInputRef.current?.matches(":focus")) {
|
||||||
|
if (app.state.openDialog) {
|
||||||
|
setAppState({
|
||||||
|
openDialog: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
searchInputRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
setAppState({
|
||||||
|
openSidebar: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.target instanceof HTMLElement &&
|
||||||
|
event.target.closest(".layer-ui__search")
|
||||||
|
) {
|
||||||
|
if (stableState.searchMatches.items.length) {
|
||||||
|
if (event.key === KEYS.ENTER) {
|
||||||
|
event.stopPropagation();
|
||||||
|
stableState.goToNextItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === KEYS.ARROW_UP) {
|
||||||
|
event.stopPropagation();
|
||||||
|
stableState.goToPreviousItem();
|
||||||
|
} else if (event.key === KEYS.ARROW_DOWN) {
|
||||||
|
event.stopPropagation();
|
||||||
|
stableState.goToNextItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// `capture` needed to prevent firing on initial open from App.tsx,
|
||||||
|
// as well as to handle events before App ones
|
||||||
|
return addEventListener(window, EVENT.KEYDOWN, eventHandler, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
}, [setAppState, stableState, app]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE:
|
||||||
|
*
|
||||||
|
* for testing purposes, we're manually focusing instead of
|
||||||
|
* setting `selectOnRender` on <TextField>
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
searchInputRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const matchCount = `${searchMatches.items.length} ${
|
||||||
|
searchMatches.items.length === 1
|
||||||
|
? t("search.singleResult")
|
||||||
|
: t("search.multipleResults")
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="layer-ui__search">
|
||||||
|
<div className="layer-ui__search-header">
|
||||||
|
<TextField
|
||||||
|
className={CLASSES.SEARCH_MENU_INPUT_WRAPPER}
|
||||||
|
value={keyword}
|
||||||
|
ref={searchInputRef}
|
||||||
|
placeholder={t("search.placeholder")}
|
||||||
|
icon={searchIcon}
|
||||||
|
onChange={(value) => {
|
||||||
|
setKeyword(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="layer-ui__search-count">
|
||||||
|
{searchMatches.items.length > 0 && (
|
||||||
|
<>
|
||||||
|
{focusIndex !== null && focusIndex > -1 ? (
|
||||||
|
<div>
|
||||||
|
{focusIndex + 1} / {matchCount}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>{matchCount}</div>
|
||||||
|
)}
|
||||||
|
<div className="result-nav">
|
||||||
|
<Button
|
||||||
|
onSelect={() => {
|
||||||
|
goToNextItem();
|
||||||
|
}}
|
||||||
|
className="result-nav-btn"
|
||||||
|
>
|
||||||
|
{collapseDownIcon}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onSelect={() => {
|
||||||
|
goToPreviousItem();
|
||||||
|
}}
|
||||||
|
className="result-nav-btn"
|
||||||
|
>
|
||||||
|
{upIcon}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{searchMatches.items.length === 0 &&
|
||||||
|
keyword &&
|
||||||
|
searchedKeywordRef.current && (
|
||||||
|
<div style={{ margin: "1rem auto" }}>{t("search.noMatch")}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MatchList
|
||||||
|
matches={searchMatches}
|
||||||
|
onItemClick={setFocusIndex}
|
||||||
|
focusIndex={focusIndex}
|
||||||
|
trimmedKeyword={keyword.trim()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListItem = (props: {
|
||||||
|
preview: SearchMatchItem["preview"];
|
||||||
|
trimmedKeyword: string;
|
||||||
|
highlighted: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) => {
|
||||||
|
const preview = [
|
||||||
|
props.preview.moreBefore ? "..." : "",
|
||||||
|
props.preview.previewText.slice(0, props.preview.indexInKeyword),
|
||||||
|
props.preview.previewText.slice(
|
||||||
|
props.preview.indexInKeyword,
|
||||||
|
props.preview.indexInKeyword + props.trimmedKeyword.length,
|
||||||
|
),
|
||||||
|
props.preview.previewText.slice(
|
||||||
|
props.preview.indexInKeyword + props.trimmedKeyword.length,
|
||||||
|
),
|
||||||
|
props.preview.moreAfter ? "..." : "",
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tabIndex={-1}
|
||||||
|
className={clsx("layer-ui__result-item", {
|
||||||
|
active: props.highlighted,
|
||||||
|
})}
|
||||||
|
onClick={props.onClick}
|
||||||
|
ref={(ref) => {
|
||||||
|
if (props.highlighted) {
|
||||||
|
ref?.scrollIntoView({ behavior: "auto", block: "nearest" });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="preview-text">
|
||||||
|
{preview.flatMap((text, idx) => (
|
||||||
|
<Fragment key={idx}>{idx === 2 ? <b>{text}</b> : text}</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MatchListProps {
|
||||||
|
matches: SearchMatches;
|
||||||
|
onItemClick: (index: number) => void;
|
||||||
|
focusIndex: number | null;
|
||||||
|
trimmedKeyword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MatchListBase = (props: MatchListProps) => {
|
||||||
|
return (
|
||||||
|
<div className="layer-ui__search-result-container">
|
||||||
|
{props.matches.items.map((searchMatch, index) => (
|
||||||
|
<ListItem
|
||||||
|
key={searchMatch.textElement.id + searchMatch.index}
|
||||||
|
trimmedKeyword={props.trimmedKeyword}
|
||||||
|
preview={searchMatch.preview}
|
||||||
|
highlighted={index === props.focusIndex}
|
||||||
|
onClick={() => props.onItemClick(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const areEqual = (prevProps: MatchListProps, nextProps: MatchListProps) => {
|
||||||
|
return (
|
||||||
|
prevProps.matches.nonce === nextProps.matches.nonce &&
|
||||||
|
prevProps.focusIndex === nextProps.focusIndex
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MatchList = memo(MatchListBase, areEqual);
|
||||||
|
|
||||||
|
const getMatchPreview = (text: string, index: number, keyword: string) => {
|
||||||
|
const WORDS_BEFORE = 2;
|
||||||
|
const WORDS_AFTER = 5;
|
||||||
|
|
||||||
|
const substrBeforeKeyword = text.slice(0, index);
|
||||||
|
const wordsBeforeKeyword = substrBeforeKeyword.split(/\s+/);
|
||||||
|
// text = "small", keyword = "mall", not complete before
|
||||||
|
// text = "small", keyword = "smal", complete before
|
||||||
|
const isKeywordCompleteBefore = substrBeforeKeyword.endsWith(" ");
|
||||||
|
const startWordIndex =
|
||||||
|
wordsBeforeKeyword.length -
|
||||||
|
WORDS_BEFORE -
|
||||||
|
1 -
|
||||||
|
(isKeywordCompleteBefore ? 0 : 1);
|
||||||
|
let wordsBeforeAsString =
|
||||||
|
wordsBeforeKeyword
|
||||||
|
.slice(startWordIndex <= 0 ? 0 : startWordIndex)
|
||||||
|
.join(" ") + (isKeywordCompleteBefore ? " " : "");
|
||||||
|
|
||||||
|
const MAX_ALLOWED_CHARS = 20;
|
||||||
|
|
||||||
|
wordsBeforeAsString =
|
||||||
|
wordsBeforeAsString.length > MAX_ALLOWED_CHARS
|
||||||
|
? wordsBeforeAsString.slice(-MAX_ALLOWED_CHARS)
|
||||||
|
: wordsBeforeAsString;
|
||||||
|
|
||||||
|
const substrAfterKeyword = text.slice(index + keyword.length);
|
||||||
|
const wordsAfter = substrAfterKeyword.split(/\s+/);
|
||||||
|
// text = "small", keyword = "mall", complete after
|
||||||
|
// text = "small", keyword = "smal", not complete after
|
||||||
|
const isKeywordCompleteAfter = !substrAfterKeyword.startsWith(" ");
|
||||||
|
const numberOfWordsToTake = isKeywordCompleteAfter
|
||||||
|
? WORDS_AFTER + 1
|
||||||
|
: WORDS_AFTER;
|
||||||
|
const wordsAfterAsString =
|
||||||
|
(isKeywordCompleteAfter ? "" : " ") +
|
||||||
|
wordsAfter.slice(0, numberOfWordsToTake).join(" ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
indexInKeyword: wordsBeforeAsString.length,
|
||||||
|
previewText: wordsBeforeAsString + keyword + wordsAfterAsString,
|
||||||
|
moreBefore: startWordIndex > 0,
|
||||||
|
moreAfter: wordsAfter.length > numberOfWordsToTake,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeWrappedText = (
|
||||||
|
wrappedText: string,
|
||||||
|
originalText: string,
|
||||||
|
): string => {
|
||||||
|
const wrappedLines = wrappedText.split("\n");
|
||||||
|
const normalizedLines: string[] = [];
|
||||||
|
let originalIndex = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < wrappedLines.length; i++) {
|
||||||
|
let currentLine = wrappedLines[i];
|
||||||
|
const nextLine = wrappedLines[i + 1];
|
||||||
|
|
||||||
|
if (nextLine) {
|
||||||
|
const nextLineIndexInOriginal = originalText.indexOf(
|
||||||
|
nextLine,
|
||||||
|
originalIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextLineIndexInOriginal > currentLine.length + originalIndex) {
|
||||||
|
let j = nextLineIndexInOriginal - (currentLine.length + originalIndex);
|
||||||
|
|
||||||
|
while (j > 0) {
|
||||||
|
currentLine += " ";
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedLines.push(currentLine);
|
||||||
|
originalIndex = originalIndex + currentLine.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedLines.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMatchedLines = (
|
||||||
|
textElement: ExcalidrawTextElement,
|
||||||
|
keyword: string,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
const normalizedText = normalizeWrappedText(
|
||||||
|
textElement.text,
|
||||||
|
textElement.originalText,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lines = normalizedText.split("\n");
|
||||||
|
|
||||||
|
const lineIndexRanges = [];
|
||||||
|
let currentIndex = 0;
|
||||||
|
let lineNumber = 0;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const startIndex = currentIndex;
|
||||||
|
const endIndex = startIndex + line.length - 1;
|
||||||
|
|
||||||
|
lineIndexRanges.push({
|
||||||
|
line,
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
lineNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move to the next line's start index
|
||||||
|
currentIndex = endIndex + 1;
|
||||||
|
lineNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let startIndex = index;
|
||||||
|
let remainingKeyword = textElement.originalText.slice(
|
||||||
|
index,
|
||||||
|
index + keyword.length,
|
||||||
|
);
|
||||||
|
const matchedLines: {
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const lineIndexRange of lineIndexRanges) {
|
||||||
|
if (remainingKeyword === "") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
startIndex >= lineIndexRange.startIndex &&
|
||||||
|
startIndex <= lineIndexRange.endIndex
|
||||||
|
) {
|
||||||
|
const matchCapacity = lineIndexRange.endIndex + 1 - startIndex;
|
||||||
|
const textToStart = lineIndexRange.line.slice(
|
||||||
|
0,
|
||||||
|
startIndex - lineIndexRange.startIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
const matchedWord = remainingKeyword.slice(0, matchCapacity);
|
||||||
|
remainingKeyword = remainingKeyword.slice(matchCapacity);
|
||||||
|
|
||||||
|
const offset = measureText(
|
||||||
|
textToStart,
|
||||||
|
getFontString(textElement),
|
||||||
|
textElement.lineHeight,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// measureText returns a non-zero width for the empty string
|
||||||
|
// which is not what we're after here, hence the check and the correction
|
||||||
|
if (textToStart === "") {
|
||||||
|
offset.width = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textElement.textAlign !== "left" && lineIndexRange.line.length > 0) {
|
||||||
|
const lineLength = measureText(
|
||||||
|
lineIndexRange.line,
|
||||||
|
getFontString(textElement),
|
||||||
|
textElement.lineHeight,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const spaceToStart =
|
||||||
|
textElement.textAlign === "center"
|
||||||
|
? (textElement.width - lineLength.width) / 2
|
||||||
|
: textElement.width - lineLength.width;
|
||||||
|
offset.width += spaceToStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width, height } = measureText(
|
||||||
|
matchedWord,
|
||||||
|
getFontString(textElement),
|
||||||
|
textElement.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
const offsetX = offset.width;
|
||||||
|
const offsetY = lineIndexRange.lineNumber * offset.height;
|
||||||
|
|
||||||
|
matchedLines.push({
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
|
||||||
|
startIndex += matchCapacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedLines;
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeSpecialCharacters = (string: string) => {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = debounce(
|
||||||
|
(
|
||||||
|
keyword: string,
|
||||||
|
app: AppClassProperties,
|
||||||
|
cb: (matchItems: SearchMatchItem[], focusIndex: number | null) => void,
|
||||||
|
) => {
|
||||||
|
if (!keyword || keyword === "") {
|
||||||
|
cb([], null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = app.scene.getNonDeletedElements();
|
||||||
|
const texts = elements.filter((el) =>
|
||||||
|
isTextElement(el),
|
||||||
|
) as ExcalidrawTextElement[];
|
||||||
|
|
||||||
|
texts.sort((a, b) => a.y - b.y);
|
||||||
|
|
||||||
|
const matchItems: SearchMatchItem[] = [];
|
||||||
|
|
||||||
|
const regex = new RegExp(escapeSpecialCharacters(keyword), "gi");
|
||||||
|
|
||||||
|
for (const textEl of texts) {
|
||||||
|
let match = null;
|
||||||
|
const text = textEl.originalText;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
const preview = getMatchPreview(text, match.index, keyword);
|
||||||
|
const matchedLines = getMatchedLines(textEl, keyword, match.index);
|
||||||
|
|
||||||
|
if (matchedLines.length > 0) {
|
||||||
|
matchItems.push({
|
||||||
|
textElement: textEl,
|
||||||
|
keyword,
|
||||||
|
preview,
|
||||||
|
index: match.index,
|
||||||
|
matchedLines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleIds = new Set(
|
||||||
|
app.visibleElements.map((visibleElement) => visibleElement.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const focusIndex =
|
||||||
|
matchItems.findIndex((matchItem) =>
|
||||||
|
visibleIds.has(matchItem.textElement.id),
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
cb(matchItems, focusIndex);
|
||||||
|
},
|
||||||
|
SEARCH_DEBOUNCE,
|
||||||
|
);
|
29
packages/excalidraw/components/SearchSidebar.tsx
Normal file
29
packages/excalidraw/components/SearchSidebar.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { SEARCH_SIDEBAR } from "../constants";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { SearchMenu } from "./SearchMenu";
|
||||||
|
import { Sidebar } from "./Sidebar/Sidebar";
|
||||||
|
|
||||||
|
export const SearchSidebar = () => {
|
||||||
|
return (
|
||||||
|
<Sidebar name={SEARCH_SIDEBAR.name} docked>
|
||||||
|
<Sidebar.Tabs>
|
||||||
|
<Sidebar.Header>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: "var(--color-primary)",
|
||||||
|
fontSize: "1.2em",
|
||||||
|
fontWeight: "bold",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
paddingRight: "1em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("search.title")}
|
||||||
|
</div>
|
||||||
|
</Sidebar.Header>
|
||||||
|
<SearchMenu />
|
||||||
|
</Sidebar.Tabs>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
};
|
@ -3,16 +3,29 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
--ExcTextField--color: var(--color-on-surface);
|
--ExcTextField--color: var(--color-on-surface);
|
||||||
--ExcTextField--label-color: var(--color-on-surface);
|
--ExcTextField--label-color: var(--color-on-surface);
|
||||||
--ExcTextField--background: transparent;
|
--ExcTextField--background: var(--color-surface-low);
|
||||||
--ExcTextField--readonly--background: var(--color-surface-high);
|
--ExcTextField--readonly--background: var(--color-surface-high);
|
||||||
--ExcTextField--readonly--color: var(--color-on-surface);
|
--ExcTextField--readonly--color: var(--color-on-surface);
|
||||||
--ExcTextField--border: var(--color-border-outline);
|
--ExcTextField--border: var(--color-gray-20);
|
||||||
--ExcTextField--readonly--border: var(--color-border-outline-variant);
|
--ExcTextField--readonly--border: var(--color-border-outline-variant);
|
||||||
--ExcTextField--border-hover: var(--color-brand-hover);
|
--ExcTextField--border-hover: var(--color-brand-hover);
|
||||||
--ExcTextField--border-active: var(--color-brand-active);
|
--ExcTextField--border-active: var(--color-brand-active);
|
||||||
--ExcTextField--placeholder: var(--color-border-outline-variant);
|
--ExcTextField--placeholder: var(--color-border-outline-variant);
|
||||||
|
|
||||||
.ExcTextField {
|
.ExcTextField {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; // 50% is not exactly in the center of the input
|
||||||
|
transform: translateY(-50%);
|
||||||
|
left: 0.75rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
color: var(--color-gray-40);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&--fullWidth {
|
&--fullWidth {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@ -37,7 +50,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 1rem;
|
|
||||||
|
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
|
|
||||||
@ -45,6 +57,8 @@
|
|||||||
border: 1px solid var(--ExcTextField--border);
|
border: 1px solid var(--ExcTextField--border);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
|
||||||
&:not(&--readonly) {
|
&:not(&--readonly) {
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--ExcTextField--border-hover);
|
border-color: var(--ExcTextField--border-hover);
|
||||||
@ -80,10 +94,6 @@
|
|||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--ExcTextField--placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:focus) {
|
&:not(:focus) {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: initial;
|
background-color: initial;
|
||||||
@ -105,5 +115,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--hasIcon .ExcTextField__input {
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,9 @@ type TextFieldProps = {
|
|||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
selectOnRender?: boolean;
|
selectOnRender?: boolean;
|
||||||
|
|
||||||
|
icon?: React.ReactNode;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
className?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isRedacted?: boolean;
|
isRedacted?: boolean;
|
||||||
} & ({ value: string } | { defaultValue: string });
|
} & ({ value: string } | { defaultValue: string });
|
||||||
@ -37,6 +39,8 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
selectOnRender,
|
selectOnRender,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
isRedacted = false,
|
isRedacted = false,
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@ -56,14 +60,16 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("ExcTextField", {
|
className={clsx("ExcTextField", className, {
|
||||||
"ExcTextField--fullWidth": fullWidth,
|
"ExcTextField--fullWidth": fullWidth,
|
||||||
|
"ExcTextField--hasIcon": !!icon,
|
||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
innerRef.current?.focus();
|
innerRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="ExcTextField__label">{label}</div>
|
{icon}
|
||||||
|
{label && <div className="ExcTextField__label">{label}</div>}
|
||||||
<div
|
<div
|
||||||
className={clsx("ExcTextField__input", {
|
className={clsx("ExcTextField__input", {
|
||||||
"ExcTextField__input--readonly": readonly,
|
"ExcTextField__input--readonly": readonly,
|
||||||
|
@ -203,6 +203,7 @@ const getRelevantAppStateProps = (
|
|||||||
snapLines: appState.snapLines,
|
snapLines: appState.snapLines,
|
||||||
zenModeEnabled: appState.zenModeEnabled,
|
zenModeEnabled: appState.zenModeEnabled,
|
||||||
editingTextElement: appState.editingTextElement,
|
editingTextElement: appState.editingTextElement,
|
||||||
|
searchMatches: appState.searchMatches,
|
||||||
});
|
});
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
|
@ -2139,3 +2139,11 @@ export const collapseUpIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const upIcon = createIcon(
|
||||||
|
<g>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M6 15l6 -6l6 6" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
LoadIcon,
|
LoadIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
save,
|
save,
|
||||||
|
searchIcon,
|
||||||
SunIcon,
|
SunIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
usersIcon,
|
usersIcon,
|
||||||
@ -27,6 +28,7 @@ import {
|
|||||||
actionLoadScene,
|
actionLoadScene,
|
||||||
actionSaveToActiveFile,
|
actionSaveToActiveFile,
|
||||||
actionShortcuts,
|
actionShortcuts,
|
||||||
|
actionToggleSearchMenu,
|
||||||
actionToggleTheme,
|
actionToggleTheme,
|
||||||
} from "../../actions";
|
} from "../../actions";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@ -40,7 +42,6 @@ import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemConten
|
|||||||
import { THEME } from "../../constants";
|
import { THEME } from "../../constants";
|
||||||
import type { Theme } from "../../element/types";
|
import type { Theme } from "../../element/types";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
|
|
||||||
import "./DefaultItems.scss";
|
import "./DefaultItems.scss";
|
||||||
|
|
||||||
export const LoadScene = () => {
|
export const LoadScene = () => {
|
||||||
@ -145,6 +146,27 @@ export const CommandPalette = (opts?: { className?: string }) => {
|
|||||||
};
|
};
|
||||||
CommandPalette.displayName = "CommandPalette";
|
CommandPalette.displayName = "CommandPalette";
|
||||||
|
|
||||||
|
export const SearchMenu = (opts?: { className?: string }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
icon={searchIcon}
|
||||||
|
data-testid="search-menu-button"
|
||||||
|
onSelect={() => {
|
||||||
|
actionManager.executeAction(actionToggleSearchMenu);
|
||||||
|
}}
|
||||||
|
shortcut={getShortcutFromShortcutName("searchMenu")}
|
||||||
|
aria-label={t("search.title")}
|
||||||
|
className={opts?.className}
|
||||||
|
>
|
||||||
|
{t("search.title")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
SearchMenu.displayName = "SearchMenu";
|
||||||
|
|
||||||
export const Help = () => {
|
export const Help = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -113,6 +113,7 @@ export const ENV = {
|
|||||||
export const CLASSES = {
|
export const CLASSES = {
|
||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
ZOOM_ACTIONS: "zoom-actions",
|
ZOOM_ACTIONS: "zoom-actions",
|
||||||
|
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -382,6 +383,10 @@ export const DEFAULT_SIDEBAR = {
|
|||||||
defaultTab: LIBRARY_SIDEBAR_TAB,
|
defaultTab: LIBRARY_SIDEBAR_TAB,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const SEARCH_SIDEBAR = {
|
||||||
|
name: "search",
|
||||||
|
};
|
||||||
|
|
||||||
export const LIBRARY_DISABLED_TYPES = new Set([
|
export const LIBRARY_DISABLED_TYPES = new Set([
|
||||||
"iframe",
|
"iframe",
|
||||||
"embeddable",
|
"embeddable",
|
||||||
|
@ -144,9 +144,9 @@
|
|||||||
--border-radius-md: 0.375rem;
|
--border-radius-md: 0.375rem;
|
||||||
--border-radius-lg: 0.5rem;
|
--border-radius-lg: 0.5rem;
|
||||||
|
|
||||||
--color-surface-high: hsl(244, 100%, 97%);
|
--color-surface-high: #f1f0ff;
|
||||||
--color-surface-mid: hsl(240 25% 96%);
|
--color-surface-mid: #f2f2f7;
|
||||||
--color-surface-low: hsl(240 25% 94%);
|
--color-surface-low: #ececf4;
|
||||||
--color-surface-lowest: #ffffff;
|
--color-surface-lowest: #ffffff;
|
||||||
--color-on-surface: #1b1b1f;
|
--color-on-surface: #1b1b1f;
|
||||||
--color-brand-hover: #5753d0;
|
--color-brand-hover: #5753d0;
|
||||||
|
@ -284,16 +284,17 @@ export const measureText = (
|
|||||||
text: string,
|
text: string,
|
||||||
font: FontString,
|
font: FontString,
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
forceAdvanceWidth?: true,
|
||||||
) => {
|
) => {
|
||||||
text = text
|
const _text = text
|
||||||
.split("\n")
|
.split("\n")
|
||||||
// replace empty lines with single space because leading/trailing empty
|
// replace empty lines with single space because leading/trailing empty
|
||||||
// lines would be stripped from computation
|
// lines would be stripped from computation
|
||||||
.map((x) => x || " ")
|
.map((x) => x || " ")
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const fontSize = parseFloat(font);
|
const fontSize = parseFloat(font);
|
||||||
const height = getTextHeight(text, fontSize, lineHeight);
|
const height = getTextHeight(_text, fontSize, lineHeight);
|
||||||
const width = getTextWidth(text, font);
|
const width = getTextWidth(_text, font, forceAdvanceWidth);
|
||||||
return { width, height };
|
return { width, height };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -162,6 +162,13 @@
|
|||||||
"hint_emptyLibrary": "Select an item on canvas to add it here, or install a library from the public repository, below.",
|
"hint_emptyLibrary": "Select an item on canvas to add it here, or install a library from the public repository, below.",
|
||||||
"hint_emptyPrivateLibrary": "Select an item on canvas to add it here."
|
"hint_emptyPrivateLibrary": "Select an item on canvas to add it here."
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"title": "Find on canvas",
|
||||||
|
"noMatch": "No matches found...",
|
||||||
|
"singleResult": "result",
|
||||||
|
"multipleResults": "results",
|
||||||
|
"placeholder": "Find text..."
|
||||||
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
"exportJSON": "Export to file",
|
"exportJSON": "Export to file",
|
||||||
@ -297,6 +304,7 @@
|
|||||||
"shapes": "Shapes"
|
"shapes": "Shapes"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
|
"dismissSearch": "Escape to dismiss search",
|
||||||
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
||||||
"linearElement": "Click to start multiple points, drag for single line",
|
"linearElement": "Click to start multiple points, drag for single line",
|
||||||
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.",
|
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.",
|
||||||
|
@ -30,8 +30,12 @@ import {
|
|||||||
shouldShowBoundingBox,
|
shouldShowBoundingBox,
|
||||||
} from "../element/transformHandles";
|
} from "../element/transformHandles";
|
||||||
import { arrayToMap, throttleRAF } from "../utils";
|
import { arrayToMap, throttleRAF } from "../utils";
|
||||||
import type { InteractiveCanvasAppState } from "../types";
|
import {
|
||||||
import { DEFAULT_TRANSFORM_HANDLE_SPACING, FRAME_STYLE } from "../constants";
|
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||||
|
FRAME_STYLE,
|
||||||
|
THEME,
|
||||||
|
} from "../constants";
|
||||||
|
import { type InteractiveCanvasAppState } from "../types";
|
||||||
|
|
||||||
import { renderSnaps } from "../renderer/renderSnaps";
|
import { renderSnaps } from "../renderer/renderSnaps";
|
||||||
|
|
||||||
@ -952,9 +956,48 @@ const _renderInteractiveScene = ({
|
|||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appState.searchMatches.forEach(({ id, focus, matchedLines }) => {
|
||||||
|
const element = elementsMap.get(id);
|
||||||
|
|
||||||
|
if (element && isTextElement(element)) {
|
||||||
|
const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
if (appState.theme === THEME.LIGHT) {
|
||||||
|
if (focus) {
|
||||||
|
context.fillStyle = "rgba(255, 124, 0, 0.4)";
|
||||||
|
} else {
|
||||||
|
context.fillStyle = "rgba(255, 226, 0, 0.4)";
|
||||||
|
}
|
||||||
|
} else if (focus) {
|
||||||
|
context.fillStyle = "rgba(229, 82, 0, 0.4)";
|
||||||
|
} else {
|
||||||
|
context.fillStyle = "rgba(99, 52, 0, 0.4)";
|
||||||
|
}
|
||||||
|
|
||||||
|
context.translate(appState.scrollX, appState.scrollY);
|
||||||
|
context.translate(cx, cy);
|
||||||
|
context.rotate(element.angle);
|
||||||
|
|
||||||
|
matchedLines.forEach((matchedLine) => {
|
||||||
|
context.fillRect(
|
||||||
|
elementX1 + matchedLine.offsetX - cx,
|
||||||
|
elementY1 + matchedLine.offsetY - cy,
|
||||||
|
matchedLine.width,
|
||||||
|
matchedLine.height,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
renderSnaps(context, appState);
|
renderSnaps(context, appState);
|
||||||
|
|
||||||
// Reset zoom
|
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
renderRemoteCursors({
|
renderRemoteCursors({
|
||||||
|
@ -866,6 +866,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -1068,6 +1069,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -1283,6 +1285,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -1613,6 +1616,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -1943,6 +1947,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -2158,6 +2163,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -2397,6 +2403,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0_copy": true,
|
"id0_copy": true,
|
||||||
},
|
},
|
||||||
@ -2699,6 +2706,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -3065,6 +3073,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -3539,6 +3548,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@ -3861,6 +3871,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@ -4185,6 +4196,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -5370,6 +5382,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -6496,6 +6509,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -7431,6 +7445,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -8339,6 +8354,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -9235,6 +9251,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
|
@ -239,6 +239,55 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
Ctrl+Shift+E
|
Ctrl+Shift+E
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label="Find on canvas"
|
||||||
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
data-testid="search-menu-button"
|
||||||
|
title="Find on canvas"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21 21l-6 -6"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__text"
|
||||||
|
>
|
||||||
|
Find on canvas
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__shortcut"
|
||||||
|
>
|
||||||
|
Ctrl+F
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-label="Help"
|
aria-label="Help"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
@ -80,6 +80,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id166": true,
|
"id166": true,
|
||||||
},
|
},
|
||||||
@ -681,6 +682,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id161": true,
|
"id161": true,
|
||||||
},
|
},
|
||||||
@ -1187,6 +1189,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -1554,6 +1557,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -1922,6 +1926,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -2185,6 +2190,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id180": true,
|
"id180": true,
|
||||||
},
|
},
|
||||||
@ -2627,6 +2633,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -2925,6 +2932,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -3208,6 +3216,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -3501,6 +3510,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -3786,6 +3796,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -4020,6 +4031,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -4278,6 +4290,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -4550,6 +4563,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -4780,6 +4794,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -5010,6 +5025,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -5238,6 +5254,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -5466,6 +5483,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -5723,6 +5741,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -6053,6 +6072,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -6477,6 +6497,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id100": true,
|
"id100": true,
|
||||||
"id101": true,
|
"id101": true,
|
||||||
@ -6855,6 +6876,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id105": true,
|
"id105": true,
|
||||||
"id106": true,
|
"id106": true,
|
||||||
@ -7170,6 +7192,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id113": true,
|
"id113": true,
|
||||||
},
|
},
|
||||||
@ -7467,6 +7490,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -7695,6 +7719,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -8049,6 +8074,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -8406,6 +8432,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id127": true,
|
"id127": true,
|
||||||
"id128": true,
|
"id128": true,
|
||||||
@ -8806,6 +8833,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -9092,6 +9120,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id124": true,
|
"id124": true,
|
||||||
},
|
},
|
||||||
@ -9356,6 +9385,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id81": true,
|
"id81": true,
|
||||||
},
|
},
|
||||||
@ -9619,6 +9649,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id83": true,
|
"id83": true,
|
||||||
},
|
},
|
||||||
@ -9852,6 +9883,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -10149,6 +10181,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id92": true,
|
"id92": true,
|
||||||
},
|
},
|
||||||
@ -10488,6 +10521,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -10725,6 +10759,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -11174,6 +11209,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id84": true,
|
"id84": true,
|
||||||
},
|
},
|
||||||
@ -11427,6 +11463,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -11665,6 +11702,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id14": true,
|
"id14": true,
|
||||||
},
|
},
|
||||||
@ -11905,6 +11943,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -12308,6 +12347,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": -50,
|
"scrollX": -50,
|
||||||
"scrollY": -50,
|
"scrollY": -50,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -12551,6 +12591,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id78": true,
|
"id78": true,
|
||||||
},
|
},
|
||||||
@ -12791,6 +12832,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id19": true,
|
"id19": true,
|
||||||
},
|
},
|
||||||
@ -13033,6 +13075,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -13277,6 +13320,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id8": true,
|
"id8": true,
|
||||||
},
|
},
|
||||||
@ -13611,6 +13655,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -13779,6 +13824,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
@ -14069,6 +14115,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -14334,6 +14381,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id11": true,
|
"id11": true,
|
||||||
},
|
},
|
||||||
@ -14609,6 +14657,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -14768,6 +14817,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id55": true,
|
"id55": true,
|
||||||
},
|
},
|
||||||
@ -15463,6 +15513,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id49": true,
|
"id49": true,
|
||||||
},
|
},
|
||||||
@ -16082,6 +16133,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id61": true,
|
"id61": true,
|
||||||
},
|
},
|
||||||
@ -16699,6 +16751,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id62": true,
|
"id62": true,
|
||||||
},
|
},
|
||||||
@ -17412,6 +17465,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id69": true,
|
"id69": true,
|
||||||
"id71": true,
|
"id71": true,
|
||||||
@ -18161,6 +18215,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id39": true,
|
"id39": true,
|
||||||
"id41": true,
|
"id41": true,
|
||||||
@ -18634,6 +18689,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id35_copy_copy": true,
|
"id35_copy_copy": true,
|
||||||
"id36_copy_copy": true,
|
"id36_copy_copy": true,
|
||||||
@ -19155,6 +19211,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -19610,6 +19667,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id27": true,
|
"id27": true,
|
||||||
},
|
},
|
||||||
|
@ -84,6 +84,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
@ -495,6 +496,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@ -893,6 +895,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id7": true,
|
"id7": true,
|
||||||
},
|
},
|
||||||
@ -1434,6 +1437,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -1636,6 +1640,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id2": true,
|
||||||
@ -2007,6 +2012,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -2241,6 +2247,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -2419,6 +2426,7 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@ -2733,6 +2741,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -2977,6 +2986,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -3216,6 +3226,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -3442,6 +3453,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -3694,6 +3706,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -3999,6 +4012,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@ -4412,6 +4426,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -4691,6 +4706,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -4939,6 +4955,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -5145,6 +5162,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -5338,6 +5356,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id2": true,
|
"id2": true,
|
||||||
},
|
},
|
||||||
@ -5719,6 +5738,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -6002,6 +6022,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -6809,6 +6830,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@ -7134,6 +7156,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -7406,6 +7429,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@ -7636,6 +7660,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -7867,6 +7892,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -8043,6 +8069,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -8219,6 +8246,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -8395,6 +8423,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -8613,6 +8642,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -8830,6 +8860,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -9020,6 +9051,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -9238,6 +9270,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -9414,6 +9447,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -9631,6 +9665,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -9807,6 +9842,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -9997,6 +10033,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -10177,6 +10214,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -10685,6 +10723,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@ -10956,6 +10995,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"scrollX": "-6.25000",
|
"scrollX": "-6.25000",
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -11080,6 +11120,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -11276,6 +11317,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -11584,6 +11626,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -11993,6 +12036,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id1": true,
|
"id1": true,
|
||||||
@ -12600,6 +12644,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
"scrollX": 60,
|
"scrollX": 60,
|
||||||
"scrollY": 60,
|
"scrollY": 60,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -12724,6 +12769,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@ -13305,6 +13351,7 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -13638,6 +13685,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
@ -13897,6 +13945,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"scrollX": 20,
|
"scrollX": 20,
|
||||||
"scrollY": "-18.53553",
|
"scrollY": "-18.53553",
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -14019,6 +14068,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
@ -14394,6 +14444,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
@ -14519,6 +14570,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
|
@ -69,8 +69,11 @@ export class Keyboard {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static keyDown = (key: string) => {
|
static keyDown = (
|
||||||
fireEvent.keyDown(document, {
|
key: string,
|
||||||
|
target: HTMLElement | Document | Window = document,
|
||||||
|
) => {
|
||||||
|
fireEvent.keyDown(target, {
|
||||||
key,
|
key,
|
||||||
ctrlKey,
|
ctrlKey,
|
||||||
shiftKey,
|
shiftKey,
|
||||||
@ -78,8 +81,11 @@ export class Keyboard {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
static keyUp = (key: string) => {
|
static keyUp = (
|
||||||
fireEvent.keyUp(document, {
|
key: string,
|
||||||
|
target: HTMLElement | Document | Window = document,
|
||||||
|
) => {
|
||||||
|
fireEvent.keyUp(target, {
|
||||||
key,
|
key,
|
||||||
ctrlKey,
|
ctrlKey,
|
||||||
shiftKey,
|
shiftKey,
|
||||||
@ -87,9 +93,9 @@ export class Keyboard {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
static keyPress = (key: string) => {
|
static keyPress = (key: string, target?: HTMLElement | Document | Window) => {
|
||||||
Keyboard.keyDown(key);
|
Keyboard.keyDown(key, target);
|
||||||
Keyboard.keyUp(key);
|
Keyboard.keyUp(key, target);
|
||||||
};
|
};
|
||||||
|
|
||||||
static codeDown = (code: string) => {
|
static codeDown = (code: string) => {
|
||||||
|
@ -11,7 +11,7 @@ export const getTextEditor = async (selector: string, waitForEditor = true) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateTextEditor = (
|
export const updateTextEditor = (
|
||||||
editor: HTMLTextAreaElement,
|
editor: HTMLTextAreaElement | HTMLInputElement,
|
||||||
value: string,
|
value: string,
|
||||||
) => {
|
) => {
|
||||||
fireEvent.change(editor, { target: { value } });
|
fireEvent.change(editor, { target: { value } });
|
||||||
|
143
packages/excalidraw/tests/search.test.tsx
Normal file
143
packages/excalidraw/tests/search.test.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { render, waitFor } from "./test-utils";
|
||||||
|
import { Excalidraw, mutateElement } from "../index";
|
||||||
|
import { CLASSES, SEARCH_SIDEBAR } from "../constants";
|
||||||
|
import { Keyboard } from "./helpers/ui";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
import { updateTextEditor } from "./queries/dom";
|
||||||
|
import { API } from "./helpers/api";
|
||||||
|
import type { ExcalidrawTextElement } from "../element/types";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
const querySearchInput = async () => {
|
||||||
|
const input =
|
||||||
|
h.app.excalidrawContainerValue.container?.querySelector<HTMLInputElement>(
|
||||||
|
`.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`,
|
||||||
|
)!;
|
||||||
|
await waitFor(() => expect(input).not.toBeNull());
|
||||||
|
return input;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("search", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally />);
|
||||||
|
h.setState({
|
||||||
|
openSidebar: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should toggle search on cmd+f", async () => {
|
||||||
|
expect(h.app.state.openSidebar).toBeNull();
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.F);
|
||||||
|
});
|
||||||
|
expect(h.app.state.openSidebar).not.toBeNull();
|
||||||
|
expect(h.app.state.openSidebar?.name).toBe(SEARCH_SIDEBAR.name);
|
||||||
|
|
||||||
|
const searchInput = await querySearchInput();
|
||||||
|
expect(searchInput.matches(":focus")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should refocus search input with cmd+f when search sidebar is still open", async () => {
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.F);
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput =
|
||||||
|
h.app.excalidrawContainerValue.container?.querySelector<HTMLInputElement>(
|
||||||
|
`.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`,
|
||||||
|
);
|
||||||
|
|
||||||
|
searchInput?.blur();
|
||||||
|
|
||||||
|
expect(h.app.state.openSidebar).not.toBeNull();
|
||||||
|
expect(searchInput?.matches(":focus")).toBe(false);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.F);
|
||||||
|
});
|
||||||
|
expect(searchInput?.matches(":focus")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match text and cycle through matches on Enter", async () => {
|
||||||
|
const scrollIntoViewMock = jest.fn();
|
||||||
|
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
|
||||||
|
|
||||||
|
API.setElements([
|
||||||
|
API.createElement({ type: "text", text: "test one" }),
|
||||||
|
API.createElement({ type: "text", text: "test two" }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(h.app.state.openSidebar).toBeNull();
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.F);
|
||||||
|
});
|
||||||
|
expect(h.app.state.openSidebar).not.toBeNull();
|
||||||
|
expect(h.app.state.openSidebar?.name).toBe(SEARCH_SIDEBAR.name);
|
||||||
|
|
||||||
|
const searchInput = await querySearchInput();
|
||||||
|
|
||||||
|
expect(searchInput.matches(":focus")).toBe(true);
|
||||||
|
|
||||||
|
updateTextEditor(searchInput, "test");
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.app.state.searchMatches.length).toBe(2);
|
||||||
|
expect(h.app.state.searchMatches[0].focus).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER, searchInput);
|
||||||
|
expect(h.app.state.searchMatches[0].focus).toBe(false);
|
||||||
|
expect(h.app.state.searchMatches[1].focus).toBe(true);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER, searchInput);
|
||||||
|
expect(h.app.state.searchMatches[0].focus).toBe(true);
|
||||||
|
expect(h.app.state.searchMatches[1].focus).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match text split across multiple lines", async () => {
|
||||||
|
const scrollIntoViewMock = jest.fn();
|
||||||
|
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
|
||||||
|
|
||||||
|
API.setElements([
|
||||||
|
API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
mutateElement(h.elements[0] as ExcalidrawTextElement, {
|
||||||
|
text: "t\ne\ns\nt \nt\ne\nx\nt \ns\np\nli\nt \ni\nn\nt\no\nm\nu\nlt\ni\np\nl\ne \nli\nn\ne\ns",
|
||||||
|
originalText: "test text split into multiple lines",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(h.app.state.openSidebar).toBeNull();
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.F);
|
||||||
|
});
|
||||||
|
expect(h.app.state.openSidebar).not.toBeNull();
|
||||||
|
expect(h.app.state.openSidebar?.name).toBe(SEARCH_SIDEBAR.name);
|
||||||
|
|
||||||
|
const searchInput = await querySearchInput();
|
||||||
|
|
||||||
|
expect(searchInput.matches(":focus")).toBe(true);
|
||||||
|
|
||||||
|
updateTextEditor(searchInput, "test");
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.app.state.searchMatches.length).toBe(1);
|
||||||
|
expect(h.app.state.searchMatches[0]?.matchedLines?.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTextEditor(searchInput, "ext spli");
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.app.state.searchMatches.length).toBe(1);
|
||||||
|
expect(h.app.state.searchMatches[0]?.matchedLines?.length).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -198,6 +198,8 @@ export type InteractiveCanvasAppState = Readonly<
|
|||||||
snapLines: AppState["snapLines"];
|
snapLines: AppState["snapLines"];
|
||||||
zenModeEnabled: AppState["zenModeEnabled"];
|
zenModeEnabled: AppState["zenModeEnabled"];
|
||||||
editingTextElement: AppState["editingTextElement"];
|
editingTextElement: AppState["editingTextElement"];
|
||||||
|
// Search matches
|
||||||
|
searchMatches: AppState["searchMatches"];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -384,8 +386,20 @@ export interface AppState {
|
|||||||
userToFollow: UserToFollow | null;
|
userToFollow: UserToFollow | null;
|
||||||
/** the socket ids of the users following the current user */
|
/** the socket ids of the users following the current user */
|
||||||
followedBy: Set<SocketId>;
|
followedBy: Set<SocketId>;
|
||||||
|
searchMatches: readonly SearchMatch[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SearchMatch = {
|
||||||
|
id: string;
|
||||||
|
focus: boolean;
|
||||||
|
matchedLines: {
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
export type UIAppState = Omit<
|
export type UIAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
| "suggestedBindings"
|
| "suggestedBindings"
|
||||||
@ -642,6 +656,9 @@ export type AppClassProperties = {
|
|||||||
getEffectiveGridSize: App["getEffectiveGridSize"];
|
getEffectiveGridSize: App["getEffectiveGridSize"];
|
||||||
setPlugins: App["setPlugins"];
|
setPlugins: App["setPlugins"];
|
||||||
plugins: App["plugins"];
|
plugins: App["plugins"];
|
||||||
|
getEditorUIOffsets: App["getEditorUIOffsets"];
|
||||||
|
visibleElements: App["visibleElements"];
|
||||||
|
excalidrawContainerValue: App["excalidrawContainerValue"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
|
@ -81,6 +81,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
|
"searchMatches": [],
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user