diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index f50d86e4f..04a473cd7 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -143,6 +143,7 @@ export const FONT_FAMILY = { "Lilita One": 7, "Comic Shanns": 8, "Liberation Sans": 9, + Assistant: 10, }; export const FONT_FAMILY_FALLBACKS = { diff --git a/packages/common/src/font-metadata.ts b/packages/common/src/font-metadata.ts index de4cc57bf..f6005ee45 100644 --- a/packages/common/src/font-metadata.ts +++ b/packages/common/src/font-metadata.ts @@ -22,8 +22,10 @@ export interface FontMetadata { }; /** flag to indicate a deprecated font */ deprecated?: true; - /** flag to indicate a server-side only font */ - serverSide?: true; + /** + * whether this is a font that users can use (= shown in font picker) + */ + private?: true; /** flag to indiccate a local-only font */ local?: true; /** flag to indicate a fallback font */ @@ -98,7 +100,16 @@ export const FONT_METADATA: Record = { descender: -434, lineHeight: 1.15, }, - serverSide: true, + private: true, + }, + [FONT_FAMILY.Assistant]: { + metrics: { + unitsPerEm: 2048, + ascender: 1021, + descender: -287, + lineHeight: 1.25, + }, + private: true, }, [FONT_FAMILY_FALLBACKS.Xiaolai]: { metrics: { diff --git a/packages/element/src/frame.ts b/packages/element/src/frame.ts index 74375a48d..3c8209954 100644 --- a/packages/element/src/frame.ts +++ b/packages/element/src/frame.ts @@ -905,13 +905,16 @@ export const shouldApplyFrameClip = ( return false; }; -export const getFrameLikeTitle = (element: ExcalidrawFrameLikeElement) => { +const DEFAULT_FRAME_NAME = "Frame"; +const DEFAULT_AI_FRAME_NAME = "AI Frame"; + +export const getDefaultFrameName = (element: ExcalidrawFrameLikeElement) => { // TODO name frames "AI" only if specific to AI frames - return element.name === null - ? isFrameElement(element) - ? "Frame" - : "AI Frame" - : element.name; + return isFrameElement(element) ? DEFAULT_FRAME_NAME : DEFAULT_AI_FRAME_NAME; +}; + +export const getFrameLikeTitle = (element: ExcalidrawFrameLikeElement) => { + return element.name === null ? getDefaultFrameName(element) : element.name; }; export const getElementsOverlappingFrame = ( diff --git a/packages/excalidraw/actions/actionToggleSearchMenu.ts b/packages/excalidraw/actions/actionToggleSearchMenu.ts index b7821bce4..e0aed070f 100644 --- a/packages/excalidraw/actions/actionToggleSearchMenu.ts +++ b/packages/excalidraw/actions/actionToggleSearchMenu.ts @@ -34,13 +34,6 @@ export const actionToggleSearchMenu = register({ `.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`, ); - if (searchInput?.matches(":focus")) { - return { - appState: { ...appState, openSidebar: null }, - captureUpdate: CaptureUpdateAction.EVENTUALLY, - }; - } - searchInput?.focus(); searchInput?.select(); return false; diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index a75745f2a..2e4ae8348 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -121,7 +121,7 @@ export const getDefaultAppState = (): Omit< followedBy: new Set(), isCropping: false, croppingElementId: null, - searchMatches: [], + searchMatches: null, }; }; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index d94d39e77..314e9f75c 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -1417,8 +1417,19 @@ class App extends React.Component { } const isDarkTheme = this.state.theme === THEME.DARK; + const nonDeletedFramesLikes = this.scene.getNonDeletedFramesLikes(); - return this.scene.getNonDeletedFramesLikes().map((f) => { + const focusedSearchMatch = + nonDeletedFramesLikes.length > 0 + ? this.state.searchMatches?.focusedId && + isFrameLikeElement( + this.scene.getElement(this.state.searchMatches.focusedId), + ) + ? this.state.searchMatches.matches.find((sm) => sm.focus) + : null + : null; + + return nonDeletedFramesLikes.map((f) => { if ( !isElementInViewport( f, @@ -1484,7 +1495,7 @@ class App extends React.Component { borderRadius: 4, boxShadow: "inset 0 0 0 1px var(--color-primary)", fontFamily: "Assistant", - fontSize: "14px", + fontSize: `${FRAME_STYLE.nameFontSize}px`, transform: `translate(-${FRAME_NAME_EDIT_PADDING}px, ${FRAME_NAME_EDIT_PADDING}px)`, color: "var(--color-gray-80)", overflow: "hidden", @@ -1528,7 +1539,10 @@ class App extends React.Component { : FRAME_STYLE.nameColorLightTheme, lineHeight: FRAME_STYLE.nameLineHeight, width: "max-content", - maxWidth: `${f.width}px`, + maxWidth: + focusedSearchMatch?.id === f.id && focusedSearchMatch?.focus + ? "none" + : `${f.width * this.state.zoom.value}px`, overflow: f.id === this.state.editingFrame ? "visible" : "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis", @@ -6405,12 +6419,17 @@ class App extends React.Component { this.maybeUnfollowRemoteUser(); if (this.state.searchMatches) { - this.setState((state) => ({ - searchMatches: state.searchMatches.map((searchMatch) => ({ - ...searchMatch, - focus: false, - })), - })); + this.setState((state) => { + return { + searchMatches: state.searchMatches && { + focusedId: null, + matches: state.searchMatches.matches.map((searchMatch) => ({ + ...searchMatch, + focus: false, + })), + }, + }; + }); this.updateEditorAtom(searchItemInFocusAtom, null); } diff --git a/packages/excalidraw/components/FontPicker/FontPickerList.tsx b/packages/excalidraw/components/FontPicker/FontPickerList.tsx index 2ec9e7d6d..6faab2917 100644 --- a/packages/excalidraw/components/FontPicker/FontPickerList.tsx +++ b/packages/excalidraw/components/FontPicker/FontPickerList.tsx @@ -99,7 +99,7 @@ export const FontPickerList = React.memo( () => Array.from(Fonts.registered.entries()) .filter( - ([_, { metadata }]) => !metadata.serverSide && !metadata.fallback, + ([_, { metadata }]) => !metadata.private && !metadata.fallback, ) .map(([familyId, { metadata, fontFaces }]) => { const fontDescriptor = { diff --git a/packages/excalidraw/components/HintViewer.tsx b/packages/excalidraw/components/HintViewer.tsx index 5072e4471..fdada9ed6 100644 --- a/packages/excalidraw/components/HintViewer.tsx +++ b/packages/excalidraw/components/HintViewer.tsx @@ -39,7 +39,7 @@ const getHints = ({ if ( appState.openSidebar?.name === DEFAULT_SIDEBAR.name && appState.openSidebar.tab === CANVAS_SEARCH_TAB && - appState.searchMatches?.length + appState.searchMatches?.matches.length ) { return t("hints.dismissSearch"); } diff --git a/packages/excalidraw/components/SearchMenu.scss b/packages/excalidraw/components/SearchMenu.scss index 1f47f9876..4f9e36a66 100644 --- a/packages/excalidraw/components/SearchMenu.scss +++ b/packages/excalidraw/components/SearchMenu.scss @@ -1,4 +1,5 @@ @import "open-color/open-color"; +@import "../css//variables.module.scss"; .excalidraw { .layer-ui__search { @@ -64,21 +65,51 @@ flex: 1 1 0; display: flex; flex-direction: column; + padding: 0 0.75rem; gap: 0.125rem; } + .layer-ui__search .collapsible-items { + gap: 2px; + } + + .layer-ui__search-result-title { + font-size: 0.875rem; + margin-bottom: 0.25rem; + display: flex; + align-items: center; + gap: 0.25rem; + font-weight: 700; + + .title-icon { + width: 0.875rem; + height: 0.875rem; + margin-right: 0.25rem; + svg g { + stroke-width: 1.25; + } + } + } + + .layer-ui__divider { + width: 100%; + margin-top: 0.25rem; + margin-bottom: 1rem; + position: relative; + } + .layer-ui__result-item { display: flex; align-items: center; - min-height: 2rem; + min-height: 1.875rem; flex: 0 0 auto; padding: 0.25rem 0.75rem; cursor: pointer; border: 1px solid transparent; outline: none; + font-size: 16px; - margin: 0 0.75rem; border-radius: var(--border-radius-md); .text-icon { diff --git a/packages/excalidraw/components/SearchMenu.tsx b/packages/excalidraw/components/SearchMenu.tsx index 3e0b31a69..4ea76b674 100644 --- a/packages/excalidraw/components/SearchMenu.tsx +++ b/packages/excalidraw/components/SearchMenu.tsx @@ -1,9 +1,15 @@ import { round } from "@excalidraw/math"; import clsx from "clsx"; import debounce from "lodash.debounce"; -import { Fragment, memo, useEffect, useRef, useState } from "react"; +import { Fragment, memo, useEffect, useMemo, useRef, useState } from "react"; -import { CLASSES, EVENT } from "@excalidraw/common"; +import { + CLASSES, + EVENT, + FONT_FAMILY, + FRAME_STYLE, + getLineHeight, +} from "@excalidraw/common"; import { isElementCompletelyInViewport } from "@excalidraw/element/sizeHelpers"; @@ -17,9 +23,17 @@ import { } from "@excalidraw/common"; import { newTextElement } from "@excalidraw/element/newElement"; -import { isTextElement } from "@excalidraw/element/typeChecks"; +import { + isFrameLikeElement, + isTextElement, +} from "@excalidraw/element/typeChecks"; -import type { ExcalidrawTextElement } from "@excalidraw/element/types"; +import { getDefaultFrameName } from "@excalidraw/element/frame"; + +import type { + ExcalidrawFrameLikeElement, + ExcalidrawTextElement, +} from "@excalidraw/element/types"; import { atom, useAtom } from "../editor-jotai"; @@ -29,11 +43,17 @@ import { t } from "../i18n"; import { useApp, useExcalidrawSetAppState } from "./App"; import { Button } from "./Button"; import { TextField } from "./TextField"; -import { collapseDownIcon, upIcon, searchIcon } from "./icons"; +import { + collapseDownIcon, + upIcon, + searchIcon, + frameToolIcon, + TextIcon, +} from "./icons"; import "./SearchMenu.scss"; -import type { AppClassProperties } from "../types"; +import type { AppClassProperties, SearchMatch } from "../types"; const searchQueryAtom = atom(""); export const searchItemInFocusAtom = atom(null); @@ -41,7 +61,7 @@ export const searchItemInFocusAtom = atom(null); const SEARCH_DEBOUNCE = 350; type SearchMatchItem = { - textElement: ExcalidrawTextElement; + element: ExcalidrawTextElement | ExcalidrawFrameLikeElement; searchQuery: SearchQuery; index: number; preview: { @@ -50,12 +70,7 @@ type SearchMatchItem = { moreBefore: boolean; moreAfter: boolean; }; - matchedLines: { - offsetX: number; - offsetY: number; - width: number; - height: number; - }[]; + matchedLines: SearchMatch["matchedLines"]; }; type SearchMatches = { @@ -103,11 +118,16 @@ export const SearchMenu = () => { searchedQueryRef.current = searchQuery; lastSceneNonceRef.current = app.scene.getSceneNonce(); setAppState({ - searchMatches: matchItems.map((searchMatch) => ({ - id: searchMatch.textElement.id, - focus: false, - matchedLines: searchMatch.matchedLines, - })), + searchMatches: matchItems.length + ? { + focusedId: null, + matches: matchItems.map((searchMatch) => ({ + id: searchMatch.element.id, + focus: false, + matchedLines: searchMatch.matchedLines, + })), + } + : null, }); }); } @@ -149,13 +169,25 @@ export const SearchMenu = () => { useEffect(() => { setAppState((state) => { + if (!state.searchMatches) { + return null; + } + + const focusedId = + focusIndex !== null + ? state.searchMatches?.matches[focusIndex]?.id || null + : null; + return { - searchMatches: state.searchMatches.map((match, index) => { - if (index === focusIndex) { - return { ...match, focus: true }; - } - return { ...match, focus: false }; - }), + searchMatches: { + focusedId, + matches: state.searchMatches.matches.map((match, index) => { + if (index === focusIndex) { + return { ...match, focus: true }; + } + return { ...match, focus: false }; + }), + }, }; }); }, [focusIndex, setAppState]); @@ -169,17 +201,21 @@ export const SearchMenu = () => { const matchAsElement = newTextElement({ text: match.searchQuery, - x: match.textElement.x + (match.matchedLines[0]?.offsetX ?? 0), - y: match.textElement.y + (match.matchedLines[0]?.offsetY ?? 0), + x: match.element.x + (match.matchedLines[0]?.offsetX ?? 0), + y: match.element.y + (match.matchedLines[0]?.offsetY ?? 0), width: match.matchedLines[0]?.width, height: match.matchedLines[0]?.height, - fontSize: match.textElement.fontSize, - fontFamily: match.textElement.fontFamily, + fontSize: isFrameLikeElement(match.element) + ? FRAME_STYLE.nameFontSize + : match.element.fontSize, + fontFamily: isFrameLikeElement(match.element) + ? FONT_FAMILY.Assistant + : match.element.fontFamily, }); const FONT_SIZE_LEGIBILITY_THRESHOLD = 14; - const fontSize = match.textElement.fontSize; + const fontSize = matchAsElement.fontSize; const isTextTiny = fontSize * zoomValue < FONT_SIZE_LEGIBILITY_THRESHOLD; @@ -233,7 +269,7 @@ export const SearchMenu = () => { searchedQueryRef.current = null; lastSceneNonceRef.current = undefined; setAppState({ - searchMatches: [], + searchMatches: null, }); setIsSearching(false); }; @@ -272,10 +308,6 @@ export const SearchMenu = () => { } searchInputRef.current?.focus(); searchInputRef.current?.select(); - } else { - setAppState({ - openSidebar: null, - }); } } @@ -336,11 +368,16 @@ export const SearchMenu = () => { searchedQueryRef.current = searchQuery; lastSceneNonceRef.current = app.scene.getSceneNonce(); setAppState({ - searchMatches: matchItems.map((searchMatch) => ({ - id: searchMatch.textElement.id, - focus: false, - matchedLines: searchMatch.matchedLines, - })), + searchMatches: matchItems.length + ? { + focusedId: null, + matches: matchItems.map((searchMatch) => ({ + id: searchMatch.element.id, + focus: false, + matchedLines: searchMatch.matchedLines, + })), + } + : null, }); setIsSearching(false); @@ -447,17 +484,56 @@ interface MatchListProps { } const MatchListBase = (props: MatchListProps) => { + const frameNameMatches = useMemo( + () => + props.matches.items.filter((match) => isFrameLikeElement(match.element)), + [props.matches], + ); + + const textMatches = useMemo( + () => props.matches.items.filter((match) => isTextElement(match.element)), + [props.matches], + ); + return ( -
- {props.matches.items.map((searchMatch, index) => ( - props.onItemClick(index)} - /> - ))} +
+ {frameNameMatches.length > 0 && ( +
+
+
{frameToolIcon}
+
{t("search.frames")}
+
+ {frameNameMatches.map((searchMatch, index) => ( + props.onItemClick(index)} + /> + ))} + + {textMatches.length > 0 &&
} +
+ )} + + {textMatches.length > 0 && ( +
+
+
{TextIcon}
+
{t("search.texts")}
+
+ {textMatches.map((searchMatch, index) => ( + props.onItemClick(index + frameNameMatches.length)} + /> + ))} +
+ )}
); }; @@ -592,12 +668,7 @@ const getMatchedLines = ( index, index + searchQuery.length, ); - const matchedLines: { - offsetX: number; - offsetY: number; - width: number; - height: number; - }[] = []; + const matchedLines: SearchMatch["matchedLines"] = []; for (const lineIndexRange of lineIndexRanges) { if (remainingQuery === "") { @@ -657,6 +728,7 @@ const getMatchedLines = ( offsetY, width, height, + showOnCanvas: true, }); startIndex += matchCapacity; @@ -666,6 +738,47 @@ const getMatchedLines = ( return matchedLines; }; +const getMatchInFrame = ( + frame: ExcalidrawFrameLikeElement, + searchQuery: SearchQuery, + index: number, + zoomValue: number, +): SearchMatch["matchedLines"] => { + const text = frame.name ?? getDefaultFrameName(frame); + const matchedText = text.slice(index, index + searchQuery.length); + + const prefixText = text.slice(0, index); + const font = getFontString({ + fontSize: FRAME_STYLE.nameFontSize, + fontFamily: FONT_FAMILY.Assistant, + }); + + const lineHeight = getLineHeight(FONT_FAMILY.Assistant); + + const offset = measureText(prefixText, font, lineHeight); + + // Correct non-zero width for empty string + if (prefixText === "") { + offset.width = 0; + } + + const matchedMetrics = measureText(matchedText, font, lineHeight); + + const offsetX = offset.width; + const offsetY = -offset.height - FRAME_STYLE.strokeWidth; + const width = matchedMetrics.width; + + return [ + { + offsetX, + offsetY, + width, + height: matchedMetrics.height, + showOnCanvas: offsetX + width <= frame.width * zoomValue, + }, + ]; +}; + const escapeSpecialCharacters = (string: string) => { return string.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&"); }; @@ -686,9 +799,14 @@ const handleSearch = debounce( isTextElement(el), ) as ExcalidrawTextElement[]; - texts.sort((a, b) => a.y - b.y); + const frames = elements.filter((el) => + isFrameLikeElement(el), + ) as ExcalidrawFrameLikeElement[]; - const matchItems: SearchMatchItem[] = []; + texts.sort((a, b) => a.y - b.y); + frames.sort((a, b) => a.y - b.y); + + const textMatches: SearchMatchItem[] = []; const regex = new RegExp(escapeSpecialCharacters(searchQuery), "gi"); @@ -701,8 +819,35 @@ const handleSearch = debounce( const matchedLines = getMatchedLines(textEl, searchQuery, match.index); if (matchedLines.length > 0) { - matchItems.push({ - textElement: textEl, + textMatches.push({ + element: textEl, + searchQuery, + preview, + index: match.index, + matchedLines, + }); + } + } + } + + const frameMatches: SearchMatchItem[] = []; + + for (const frame of frames) { + let match = null; + const name = frame.name ?? getDefaultFrameName(frame); + + while ((match = regex.exec(name)) !== null) { + const preview = getMatchPreview(name, match.index, searchQuery); + const matchedLines = getMatchInFrame( + frame, + searchQuery, + match.index, + app.state.zoom.value, + ); + + if (matchedLines.length > 0) { + frameMatches.push({ + element: frame, searchQuery, preview, index: match.index, @@ -716,9 +861,12 @@ const handleSearch = debounce( app.visibleElements.map((visibleElement) => visibleElement.id), ); + // putting frame matches first + const matchItems: SearchMatchItem[] = [...frameMatches, ...textMatches]; + const focusIndex = matchItems.findIndex((matchItem) => - visibleIds.has(matchItem.textElement.id), + visibleIds.has(matchItem.element.id), ) ?? null; cb(matchItems, focusIndex); diff --git a/packages/excalidraw/components/Stats/Collapsible.tsx b/packages/excalidraw/components/Stats/Collapsible.tsx index 13d476d2a..4d73b81bb 100644 --- a/packages/excalidraw/components/Stats/Collapsible.tsx +++ b/packages/excalidraw/components/Stats/Collapsible.tsx @@ -10,6 +10,7 @@ interface CollapsibleProps { openTrigger: () => void; children: React.ReactNode; className?: string; + showCollapsedIcon?: boolean; } const Collapsible = ({ @@ -18,6 +19,7 @@ const Collapsible = ({ openTrigger, children, className, + showCollapsedIcon = true, }: CollapsibleProps) => { return ( <> @@ -32,7 +34,9 @@ const Collapsible = ({ onClick={openTrigger} > {label} - + {showCollapsedIcon && ( + + )}
{open && (
diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index 9a5211027..f89fa5f1f 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -184,7 +184,9 @@ "noMatch": "No matches found...", "singleResult": "result", "multipleResults": "results", - "placeholder": "Find text on canvas..." + "placeholder": "Find text on canvas...", + "frames": "Frames", + "texts": "Texts" }, "buttons": { "clearReset": "Reset the canvas", diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index dcef13209..27bd31b09 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -1040,10 +1040,10 @@ const _renderInteractiveScene = ({ context.restore(); } - appState.searchMatches.forEach(({ id, focus, matchedLines }) => { + appState.searchMatches?.matches.forEach(({ id, focus, matchedLines }) => { const element = elementsMap.get(id); - if (element && isTextElement(element)) { + if (element) { const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords( element, elementsMap, @@ -1063,17 +1063,20 @@ const _renderInteractiveScene = ({ context.fillStyle = "rgba(99, 52, 0, 0.4)"; } + const zoomFactor = isFrameLikeElement(element) ? appState.zoom.value : 1; + 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, - ); + (matchedLine.showOnCanvas || focus) && + context.fillRect( + elementX1 + matchedLine.offsetX / zoomFactor - cx, + elementY1 + matchedLine.offsetY / zoomFactor - cy, + matchedLine.width / zoomFactor, + matchedLine.height / zoomFactor, + ); }); context.restore(); diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 87fb23abd..6c258c621 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -961,7 +961,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id1": true, @@ -1159,7 +1159,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -1373,7 +1373,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -1704,7 +1704,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -2035,7 +2035,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -2249,7 +2249,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -2488,7 +2488,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id3": true, }, @@ -2790,7 +2790,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id3": true, @@ -3158,7 +3158,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -3639,7 +3639,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id3": true, }, @@ -3962,7 +3962,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id3": true, }, @@ -4287,7 +4287,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id3": true, @@ -5566,7 +5566,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id3": true, @@ -6785,7 +6785,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id3": true, @@ -7719,7 +7719,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -8714,7 +8714,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -9706,7 +9706,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id1": true, }, diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 30bfe5c63..5e3f00c31 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -84,7 +84,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id691": true, }, @@ -673,7 +673,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id668": true, }, @@ -1181,7 +1181,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -1547,7 +1547,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -1914,7 +1914,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -2176,7 +2176,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id740": true, }, @@ -2614,7 +2614,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -2911,7 +2911,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -3193,7 +3193,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -3485,7 +3485,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -3769,7 +3769,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -4002,7 +4002,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -4259,7 +4259,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -4530,7 +4530,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -4759,7 +4759,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -4988,7 +4988,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -5215,7 +5215,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -5442,7 +5442,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -5699,7 +5699,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -6030,7 +6030,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -6457,7 +6457,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id433": true, "id434": true, @@ -6836,7 +6836,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id452": true, "id453": true, @@ -7151,7 +7151,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id486": true, }, @@ -7450,7 +7450,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -7678,7 +7678,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -8032,7 +8032,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -8389,7 +8389,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id542": true, "id545": true, @@ -8792,7 +8792,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -9077,7 +9077,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id531": true, }, @@ -9341,7 +9341,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id333": true, }, @@ -9606,7 +9606,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id342": true, }, @@ -9839,7 +9839,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -10135,7 +10135,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id379": true, }, @@ -10475,7 +10475,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -10712,7 +10712,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -11157,7 +11157,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id349": true, }, @@ -11413,7 +11413,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -11650,7 +11650,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id50": true, }, @@ -11889,7 +11889,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -12293,7 +12293,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "resizingElement": null, "scrollX": -50, "scrollY": -50, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -12535,7 +12535,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id323": true, }, @@ -12774,7 +12774,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id70": true, }, @@ -13015,7 +13015,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -13260,7 +13260,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id18": true, }, @@ -13596,7 +13596,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -13763,7 +13763,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id4": true, "id5": true, @@ -14052,7 +14052,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -14316,7 +14316,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id29": true, }, @@ -14595,7 +14595,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -14754,7 +14754,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id243": true, }, @@ -15453,7 +15453,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id225": true, }, @@ -16071,7 +16071,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id262": true, }, @@ -16687,7 +16687,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id275": true, }, @@ -17402,7 +17402,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id297": true, "id299": true, @@ -18153,7 +18153,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id189": true, "id195": true, @@ -18631,7 +18631,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id184": true, "id186": true, @@ -19152,7 +19152,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -19611,7 +19611,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "resizingElement": null, "scrollX": 0, "scrollY": 0, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id119": true, }, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index 599c1c7f6..67d2c5de2 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -88,7 +88,7 @@ exports[`given element A and group of elements B and given both are selected whe "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id6": true, @@ -508,7 +508,7 @@ exports[`given element A and group of elements B and given both are selected whe "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id3": true, }, @@ -914,7 +914,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id19": true, }, @@ -1468,7 +1468,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -1673,7 +1673,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id6": true, @@ -2051,7 +2051,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id4": true, }, @@ -2283,7 +2283,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = ` "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -2463,7 +2463,7 @@ exports[`regression tests > can drag element that covers another element, while "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id3": true, }, @@ -2782,7 +2782,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -3031,7 +3031,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -3270,7 +3270,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -3500,7 +3500,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`] "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -3756,7 +3756,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id3": true, @@ -4066,7 +4066,7 @@ exports[`regression tests > deleting last but one element in editing group shoul "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id3": true, }, @@ -4497,7 +4497,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -4780,7 +4780,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -5032,7 +5032,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -5240,7 +5240,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -5436,7 +5436,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id6": true, }, @@ -5824,7 +5824,7 @@ exports[`regression tests > drags selected elements from point inside common bou "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id3": true, @@ -6112,7 +6112,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1` "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -6933,7 +6933,7 @@ exports[`regression tests > given a group of selected elements with an element t "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id3": true, }, @@ -7264,7 +7264,7 @@ exports[`regression tests > given a selected element A and a not selected elemen "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id3": true, @@ -7541,7 +7541,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id1": true, }, @@ -7774,7 +7774,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -8008,7 +8008,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -8186,7 +8186,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -8364,7 +8364,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -8542,7 +8542,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -8763,7 +8763,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -8983,7 +8983,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -9175,7 +9175,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -9396,7 +9396,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -9574,7 +9574,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -9794,7 +9794,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -9972,7 +9972,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -10164,7 +10164,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -10346,7 +10346,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id16": true, "id18": true, @@ -10842,7 +10842,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id3": true, }, @@ -11118,7 +11118,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = ` "scrollX": "-6.25000", "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -11241,7 +11241,7 @@ exports[`regression tests > shift click on selected element should deselect it o "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -11440,7 +11440,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id3": true, @@ -11754,7 +11754,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id3": true, @@ -12170,7 +12170,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, "id15": true, @@ -12790,7 +12790,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a "scrollX": 60, "scrollY": 60, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -12913,7 +12913,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`] "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id3": true, }, @@ -13508,7 +13508,7 @@ exports[`regression tests > switches from group of selected elements to another "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -13846,7 +13846,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id0": true, }, @@ -14108,7 +14108,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`] "scrollX": 20, "scrollY": "-18.53553", "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -14229,7 +14229,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": { "id3": true, }, @@ -14612,7 +14612,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -14736,7 +14736,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, diff --git a/packages/excalidraw/tests/search.test.tsx b/packages/excalidraw/tests/search.test.tsx index 3a42cff62..b9a60bf59 100644 --- a/packages/excalidraw/tests/search.test.tsx +++ b/packages/excalidraw/tests/search.test.tsx @@ -7,7 +7,10 @@ import { KEYS, } from "@excalidraw/common"; -import type { ExcalidrawTextElement } from "@excalidraw/element/types"; +import type { + ExcalidrawFrameLikeElement, + ExcalidrawTextElement, +} from "@excalidraw/element/types"; import { Excalidraw } from "../index"; @@ -97,17 +100,17 @@ describe("search", () => { updateTextEditor(searchInput, "test"); await waitFor(() => { - expect(h.app.state.searchMatches.length).toBe(2); - expect(h.app.state.searchMatches[0].focus).toBe(true); + expect(h.app.state.searchMatches?.matches.length).toBe(2); + expect(h.app.state.searchMatches?.matches[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); + expect(h.app.state.searchMatches?.matches[0].focus).toBe(false); + expect(h.app.state.searchMatches?.matches[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); + expect(h.app.state.searchMatches?.matches[0].focus).toBe(true); + expect(h.app.state.searchMatches?.matches[1].focus).toBe(false); }); it("should match text split across multiple lines", async () => { @@ -142,15 +145,53 @@ describe("search", () => { updateTextEditor(searchInput, "test"); await waitFor(() => { - expect(h.app.state.searchMatches.length).toBe(1); - expect(h.app.state.searchMatches[0]?.matchedLines?.length).toBe(4); + expect(h.app.state.searchMatches?.matches.length).toBe(1); + expect(h.app.state.searchMatches?.matches[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); + expect(h.app.state.searchMatches?.matches.length).toBe(1); + expect(h.app.state.searchMatches?.matches[0]?.matchedLines?.length).toBe( + 6, + ); + }); + }); + + it("should match frame names", async () => { + const scrollIntoViewMock = jest.fn(); + window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; + + API.setElements([ + API.createElement({ + type: "frame", + }), + ]); + + API.updateElement(h.elements[0] as ExcalidrawFrameLikeElement, { + name: "Frame: name test for frame, yes, frame!", + }); + + 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(DEFAULT_SIDEBAR.name); + expect(h.app.state.openSidebar?.tab).toBe(CANVAS_SEARCH_TAB); + + const searchInput = await querySearchInput(); + + expect(searchInput.matches(":focus")).toBe(true); + + updateTextEditor(searchInput, "frame"); + + await waitFor(() => { + expect(h.app.state.searchMatches?.matches.length).toBe(3); }); }); }); diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 146906d50..7c18299e9 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -432,10 +432,14 @@ export interface AppState { isCropping: boolean; croppingElementId: ExcalidrawElement["id"] | null; - searchMatches: readonly SearchMatch[]; + /** null if no search matches found / search closed */ + searchMatches: Readonly<{ + focusedId: ExcalidrawElement["id"] | null; + matches: readonly SearchMatch[]; + }> | null; } -type SearchMatch = { +export type SearchMatch = { id: string; focus: boolean; matchedLines: { @@ -443,6 +447,7 @@ type SearchMatch = { offsetY: number; width: number; height: number; + showOnCanvas: boolean; }[]; }; diff --git a/packages/utils/tests/__snapshots__/export.test.ts.snap b/packages/utils/tests/__snapshots__/export.test.ts.snap index 91108a600..baa5e3d31 100644 --- a/packages/utils/tests/__snapshots__/export.test.ts.snap +++ b/packages/utils/tests/__snapshots__/export.test.ts.snap @@ -85,7 +85,7 @@ exports[`exportToSvg > with default arguments 1`] = ` "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "searchMatches": [], + "searchMatches": null, "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {},