فهرست منبع

feat: add `props.renderScrollbars` (#9399)

* Expose renderScrollbars to AppState

* fix: scrollbar rendering should use al renderable elements

* remove `appState.renderScrollbars`

* clean unused

---------

Co-authored-by: dwelle <[email protected]>
Jack Walsh 3 ماه پیش
والد
کامیت
5fc13e4309

+ 1 - 0
dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx

@@ -31,6 +31,7 @@ All `props` are _optional_.
 | [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
 | [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
 | [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean &#124; undefined)</code> | \_ | use for custom src url validation |
 | [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean &#124; undefined)</code> | \_ | use for custom src url validation |
 | [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
 | [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
+| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
 
 
 ### Storing custom data on Excalidraw elements
 ### Storing custom data on Excalidraw elements
 
 

+ 10 - 0
examples/with-script-in-browser/components/ExampleApp.tsx

@@ -104,6 +104,7 @@ export default function ExampleApp({
   const [viewModeEnabled, setViewModeEnabled] = useState(false);
   const [viewModeEnabled, setViewModeEnabled] = useState(false);
   const [zenModeEnabled, setZenModeEnabled] = useState(false);
   const [zenModeEnabled, setZenModeEnabled] = useState(false);
   const [gridModeEnabled, setGridModeEnabled] = useState(false);
   const [gridModeEnabled, setGridModeEnabled] = useState(false);
+  const [renderScrollbars, setRenderScrollbars] = useState(false);
   const [blobUrl, setBlobUrl] = useState<string>("");
   const [blobUrl, setBlobUrl] = useState<string>("");
   const [canvasUrl, setCanvasUrl] = useState<string>("");
   const [canvasUrl, setCanvasUrl] = useState<string>("");
   const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
   const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
@@ -192,6 +193,7 @@ export default function ExampleApp({
         }) => setPointerData(payload),
         }) => setPointerData(payload),
         viewModeEnabled,
         viewModeEnabled,
         zenModeEnabled,
         zenModeEnabled,
+        renderScrollbars,
         gridModeEnabled,
         gridModeEnabled,
         theme,
         theme,
         name: "Custom name of drawing",
         name: "Custom name of drawing",
@@ -710,6 +712,14 @@ export default function ExampleApp({
             />
             />
             Grid mode
             Grid mode
           </label>
           </label>
+          <label>
+            <input
+              type="checkbox"
+              checked={renderScrollbars}
+              onChange={() => setRenderScrollbars(!renderScrollbars)}
+            />
+            Render scrollbars
+          </label>
           <label>
           <label>
             <input
             <input
               type="checkbox"
               type="checkbox"

+ 15 - 0
packages/common/src/utils.ts

@@ -1218,3 +1218,18 @@ export const elementCenterPoint = (
 
 
   return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
   return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
 };
 };
+
+/** hack for Array.isArray type guard not working with readonly value[] */
+export const isReadonlyArray = (value?: any): value is readonly any[] => {
+  return Array.isArray(value);
+};
+
+export const sizeOf = (
+  value: readonly number[] | Readonly<Map<any, any>> | Record<any, any>,
+): number => {
+  return isReadonlyArray(value)
+    ? value.length
+    : value instanceof Map
+    ? value.size
+    : Object.keys(value).length;
+};

+ 9 - 3
packages/element/src/bounds.ts

@@ -1,6 +1,11 @@
 import rough from "roughjs/bin/rough";
 import rough from "roughjs/bin/rough";
 
 
-import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common";
+import {
+  rescalePoints,
+  arrayToMap,
+  invariant,
+  sizeOf,
+} from "@excalidraw/common";
 
 
 import {
 import {
   degreesToRadians,
   degreesToRadians,
@@ -57,6 +62,7 @@ import type {
   ElementsMap,
   ElementsMap,
   ExcalidrawRectanguloidElement,
   ExcalidrawRectanguloidElement,
   ExcalidrawEllipseElement,
   ExcalidrawEllipseElement,
+  ElementsMapOrArray,
 } from "./types";
 } from "./types";
 import type { Drawable, Op } from "roughjs/bin/core";
 import type { Drawable, Op } from "roughjs/bin/core";
 import type { Point as RoughPoint } from "roughjs/bin/geometry";
 import type { Point as RoughPoint } from "roughjs/bin/geometry";
@@ -938,10 +944,10 @@ export const getElementBounds = (
 };
 };
 
 
 export const getCommonBounds = (
 export const getCommonBounds = (
-  elements: readonly ExcalidrawElement[],
+  elements: ElementsMapOrArray,
   elementsMap?: ElementsMap,
   elementsMap?: ElementsMap,
 ): Bounds => {
 ): Bounds => {
-  if (!elements.length) {
+  if (!sizeOf(elements)) {
     return [0, 0, 0, 0];
     return [0, 0, 0, 0];
   }
   }
 
 

+ 3 - 0
packages/excalidraw/components/App.tsx

@@ -1830,6 +1830,9 @@ class App extends React.Component<AppProps, AppState> {
                           }
                           }
                           scale={window.devicePixelRatio}
                           scale={window.devicePixelRatio}
                           appState={this.state}
                           appState={this.state}
+                          renderScrollbars={
+                            this.props.renderScrollbars === true
+                          }
                           device={this.device}
                           device={this.device}
                           renderInteractiveSceneCallback={
                           renderInteractiveSceneCallback={
                             this.renderInteractiveSceneCallback
                             this.renderInteractiveSceneCallback

+ 4 - 2
packages/excalidraw/components/canvases/InteractiveCanvas.tsx

@@ -34,6 +34,7 @@ type InteractiveCanvasProps = {
   selectionNonce: number | undefined;
   selectionNonce: number | undefined;
   scale: number;
   scale: number;
   appState: InteractiveCanvasAppState;
   appState: InteractiveCanvasAppState;
+  renderScrollbars: boolean;
   device: Device;
   device: Device;
   renderInteractiveSceneCallback: (
   renderInteractiveSceneCallback: (
     data: RenderInteractiveSceneCallback,
     data: RenderInteractiveSceneCallback,
@@ -143,7 +144,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
           remotePointerUsernames,
           remotePointerUsernames,
           remotePointerUserStates,
           remotePointerUserStates,
           selectionColor,
           selectionColor,
-          renderScrollbars: false,
+          renderScrollbars: props.renderScrollbars,
         },
         },
         device: props.device,
         device: props.device,
         callback: props.renderInteractiveSceneCallback,
         callback: props.renderInteractiveSceneCallback,
@@ -230,7 +231,8 @@ const areEqual = (
     // on appState)
     // on appState)
     prevProps.elementsMap !== nextProps.elementsMap ||
     prevProps.elementsMap !== nextProps.elementsMap ||
     prevProps.visibleElements !== nextProps.visibleElements ||
     prevProps.visibleElements !== nextProps.visibleElements ||
-    prevProps.selectedElements !== nextProps.selectedElements
+    prevProps.selectedElements !== nextProps.selectedElements ||
+    prevProps.renderScrollbars !== nextProps.renderScrollbars
   ) {
   ) {
     return false;
     return false;
   }
   }

+ 2 - 0
packages/excalidraw/index.tsx

@@ -53,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
     renderEmbeddable,
     renderEmbeddable,
     aiEnabled,
     aiEnabled,
     showDeprecatedFonts,
     showDeprecatedFonts,
+    renderScrollbars,
   } = props;
   } = props;
 
 
   const canvasActions = props.UIOptions?.canvasActions;
   const canvasActions = props.UIOptions?.canvasActions;
@@ -143,6 +144,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
           renderEmbeddable={renderEmbeddable}
           renderEmbeddable={renderEmbeddable}
           aiEnabled={aiEnabled !== false}
           aiEnabled={aiEnabled !== false}
           showDeprecatedFonts={showDeprecatedFonts}
           showDeprecatedFonts={showDeprecatedFonts}
+          renderScrollbars={renderScrollbars}
         >
         >
           {children}
           {children}
         </App>
         </App>

+ 1 - 1
packages/excalidraw/renderer/interactiveScene.ts

@@ -1182,7 +1182,7 @@ const _renderInteractiveScene = ({
   let scrollBars;
   let scrollBars;
   if (renderConfig.renderScrollbars) {
   if (renderConfig.renderScrollbars) {
     scrollBars = getScrollBars(
     scrollBars = getScrollBars(
-      visibleElements,
+      elementsMap,
       normalizedWidth,
       normalizedWidth,
       normalizedHeight,
       normalizedHeight,
       appState,
       appState,

+ 4 - 5
packages/excalidraw/scene/Scene.ts

@@ -6,6 +6,7 @@ import {
   toBrandedType,
   toBrandedType,
   isDevEnv,
   isDevEnv,
   isTestEnv,
   isTestEnv,
+  isReadonlyArray,
 } from "@excalidraw/common";
 } from "@excalidraw/common";
 import { isNonDeletedElement } from "@excalidraw/element";
 import { isNonDeletedElement } from "@excalidraw/element";
 import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
 import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
@@ -292,11 +293,9 @@ class Scene {
   }
   }
 
 
   replaceAllElements(nextElements: ElementsMapOrArray) {
   replaceAllElements(nextElements: ElementsMapOrArray) {
-    const _nextElements =
-      // ts doesn't like `Array.isArray` of `instanceof Map`
-      nextElements instanceof Array
-        ? nextElements
-        : Array.from(nextElements.values());
+    const _nextElements = isReadonlyArray(nextElements)
+      ? nextElements
+      : Array.from(nextElements.values());
     const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
     const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
 
 
     validateIndicesThrottled(_nextElements);
     validateIndicesThrottled(_nextElements);

+ 3 - 5
packages/excalidraw/scene/scrollbars.ts

@@ -2,24 +2,22 @@ import { getGlobalCSSVariable } from "@excalidraw/common";
 
 
 import { getCommonBounds } from "@excalidraw/element/bounds";
 import { getCommonBounds } from "@excalidraw/element/bounds";
 
 
-import type { ExcalidrawElement } from "@excalidraw/element/types";
-
 import { getLanguage } from "../i18n";
 import { getLanguage } from "../i18n";
 
 
 import type { InteractiveCanvasAppState } from "../types";
 import type { InteractiveCanvasAppState } from "../types";
-import type { ScrollBars } from "./types";
+import type { RenderableElementsMap, ScrollBars } from "./types";
 
 
 export const SCROLLBAR_MARGIN = 4;
 export const SCROLLBAR_MARGIN = 4;
 export const SCROLLBAR_WIDTH = 6;
 export const SCROLLBAR_WIDTH = 6;
 export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
 export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
 
 
 export const getScrollBars = (
 export const getScrollBars = (
-  elements: readonly ExcalidrawElement[],
+  elements: RenderableElementsMap,
   viewportWidth: number,
   viewportWidth: number,
   viewportHeight: number,
   viewportHeight: number,
   appState: InteractiveCanvasAppState,
   appState: InteractiveCanvasAppState,
 ): ScrollBars => {
 ): ScrollBars => {
-  if (!elements.length) {
+  if (!elements.size) {
     return {
     return {
       horizontal: null,
       horizontal: null,
       vertical: null,
       vertical: null,

+ 1 - 0
packages/excalidraw/types.ts

@@ -601,6 +601,7 @@ export interface ExcalidrawProps {
   ) => JSX.Element | null;
   ) => JSX.Element | null;
   aiEnabled?: boolean;
   aiEnabled?: boolean;
   showDeprecatedFonts?: boolean;
   showDeprecatedFonts?: boolean;
+  renderScrollbars?: boolean;
 }
 }
 
 
 export type SceneData = {
 export type SceneData = {