Move constants to options prop (#3799)
Another go at #3628 & #3783. This moves (most) constants into `editor.options`, configurable by the `options` prop on the tldraw component. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `feature` — New feature ### Release Notes You can now override many options which were previously hard-coded constants. Pass an `options` prop into the tldraw component to change the maximum number of pages, grid steps, or other previously hard-coded values. See `TldrawOptions` for more
This commit is contained in:
parent
19f8d4248c
commit
a457a39081
37 changed files with 347 additions and 267 deletions
|
@ -0,0 +1,15 @@
|
|||
import { Tldraw, TldrawOptions } from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
|
||||
const options: Partial<TldrawOptions> = {
|
||||
maxPages: 3,
|
||||
animationMediumMs: 5000,
|
||||
}
|
||||
|
||||
export default function CustomOptionsExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw persistenceKey="example" options={options} />
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/custom-options/README.md
Normal file
11
apps/examples/src/examples/custom-options/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: Custom options
|
||||
component: ./CustomOptionsExample.tsx
|
||||
category: basic
|
||||
priority: 5
|
||||
---
|
||||
|
||||
Use the `options` property to override tldraw's options. In this example, we limit the maximum
|
||||
number of pages to 3, and slow down camera animations like zoom in and zoom out.
|
||||
|
||||
---
|
|
@ -89,12 +89,6 @@ import { whyAmIRunning } from '@tldraw/state';
|
|||
// @public
|
||||
export function angleDistance(fromAngle: number, toAngle: number, direction: number): number;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const ANIMATION_MEDIUM_MS = 320;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const ANIMATION_SHORT_MS = 80;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function applyRotationToSnapshotShapes({ delta, editor, snapshot, stage, }: {
|
||||
delta: number;
|
||||
|
@ -391,9 +385,6 @@ export class Box {
|
|||
// @public (undocumented)
|
||||
export type BoxLike = Box | BoxModel;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const CAMERA_SLIDE_FRICTION = 0.09;
|
||||
|
||||
// @public (undocumented)
|
||||
export function canonicalizeRotation(a: number): number;
|
||||
|
||||
|
@ -607,6 +598,50 @@ export function DefaultSpinner(): JSX_2.Element;
|
|||
// @public (undocumented)
|
||||
export const DefaultSvgDefs: () => null;
|
||||
|
||||
// @public (undocumented)
|
||||
export const defaultTldrawOptions: {
|
||||
readonly adjacentShapeMargin: 10;
|
||||
readonly animationMediumMs: 320;
|
||||
readonly cameraMovingTimoutMs: 64;
|
||||
readonly cameraSlideFriction: 0.09;
|
||||
readonly coarseDragDistanceSquared: 36;
|
||||
readonly coarseHandleRadius: 20;
|
||||
readonly coarsePointerWidth: 12;
|
||||
readonly collaboratorCheckIntervalMs: 1200;
|
||||
readonly collaboratorIdleTimeoutMs: 3000;
|
||||
readonly collaboratorInactiveTimeoutMs: 60000;
|
||||
readonly defaultSvgPadding: 32;
|
||||
readonly doubleClickDurationMs: 450;
|
||||
readonly dragDistanceSquared: 16;
|
||||
readonly edgeScrollDistance: 8;
|
||||
readonly edgeScrollSpeed: 20;
|
||||
readonly followChaseViewportSnap: 2;
|
||||
readonly gridSteps: readonly [{
|
||||
readonly mid: 0.15;
|
||||
readonly min: -1;
|
||||
readonly step: 64;
|
||||
}, {
|
||||
readonly mid: 0.375;
|
||||
readonly min: 0.05;
|
||||
readonly step: 16;
|
||||
}, {
|
||||
readonly mid: 1;
|
||||
readonly min: 0.15;
|
||||
readonly step: 4;
|
||||
}, {
|
||||
readonly mid: 2.5;
|
||||
readonly min: 0.7;
|
||||
readonly step: 1;
|
||||
}];
|
||||
readonly handleRadius: 12;
|
||||
readonly hitTestMargin: 8;
|
||||
readonly longPressDurationMs: 500;
|
||||
readonly maxPages: 40;
|
||||
readonly maxShapesPerPage: 4000;
|
||||
readonly multiClickDurationMs: 200;
|
||||
readonly textShadowLod: 0.35;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const defaultUserPreferences: Readonly<{
|
||||
animationSpeed: 0 | 1;
|
||||
|
@ -622,12 +657,6 @@ export const defaultUserPreferences: Readonly<{
|
|||
// @public
|
||||
export function degreesToRadians(d: number): number;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DOUBLE_CLICK_DURATION = 450;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DRAG_DISTANCE = 16;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EASINGS: {
|
||||
readonly easeInCubic: (t: number) => number;
|
||||
|
@ -683,7 +712,7 @@ export class Edge2d extends Geometry2d {
|
|||
|
||||
// @public (undocumented)
|
||||
export class Editor extends EventEmitter<TLEventMap> {
|
||||
constructor({ store, user, shapeUtils, bindingUtils, tools, getContainer, cameraOptions, initialState, autoFocus, inferDarkMode, }: TLEditorOptions);
|
||||
constructor({ store, user, shapeUtils, bindingUtils, tools, getContainer, cameraOptions, initialState, autoFocus, inferDarkMode, options, }: TLEditorOptions);
|
||||
addOpenMenu(id: string): this;
|
||||
alignShapes(shapes: TLShape[] | TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
|
||||
animateShape(partial: null | TLShapePartial | undefined, opts?: Partial<{
|
||||
|
@ -998,6 +1027,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
mark(markId?: string): this;
|
||||
moveShapesToPage(shapes: TLShape[] | TLShapeId[], pageId: TLPageId): this;
|
||||
nudgeShapes(shapes: TLShape[] | TLShapeId[], offset: VecLike): this;
|
||||
// (undocumented)
|
||||
readonly options: TldrawOptions;
|
||||
packShapes(shapes: TLShape[] | TLShapeId[], gap: number): this;
|
||||
pageToScreen(point: VecLike): Vec;
|
||||
pageToViewport(point: VecLike): Vec;
|
||||
|
@ -1051,7 +1082,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
readonly sideEffects: StoreSideEffects<TLRecord>;
|
||||
slideCamera(opts?: {
|
||||
direction: VecLike;
|
||||
friction: number;
|
||||
friction?: number | undefined;
|
||||
speed: number;
|
||||
speedThreshold?: number | undefined;
|
||||
}): this;
|
||||
|
@ -1281,13 +1312,6 @@ export function getSvgPathFromPoints(points: VecLike[], closed?: boolean): strin
|
|||
// @public (undocumented)
|
||||
export function getUserPreferences(): TLUserPreferences;
|
||||
|
||||
// @public (undocumented)
|
||||
export const GRID_STEPS: {
|
||||
mid: number;
|
||||
min: number;
|
||||
step: number;
|
||||
}[];
|
||||
|
||||
// @public (undocumented)
|
||||
export class Group2d extends Geometry2d {
|
||||
constructor(config: Omit<Geometry2dOptions, 'isClosed' | 'isFilled'> & {
|
||||
|
@ -1409,9 +1433,6 @@ export class HistoryManager<R extends UnknownRecord> {
|
|||
undo: () => this;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const HIT_TEST_MARGIN = 8;
|
||||
|
||||
// @public (undocumented)
|
||||
export function HTMLContainer({ children, className, ...rest }: HTMLContainerProps): JSX_2.Element;
|
||||
|
||||
|
@ -1589,18 +1610,9 @@ export interface MatModel {
|
|||
f: number;
|
||||
}
|
||||
|
||||
// @internal (undocumented)
|
||||
export const MAX_PAGES = 40;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const MAX_SHAPES_PER_PAGE = 4000;
|
||||
|
||||
// @public
|
||||
export function moveCameraWhenCloseToEdge(editor: Editor): void;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const MULTI_CLICK_DURATION = 200;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function normalizeWheel(event: React.WheelEvent<HTMLElement> | WheelEvent): {
|
||||
x: number;
|
||||
|
@ -2075,9 +2087,6 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|||
// @public (undocumented)
|
||||
export const stopEventPropagation: (e: any) => any;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const SVG_PADDING = 32;
|
||||
|
||||
// @public (undocumented)
|
||||
export function SVGContainer({ children, className, ...rest }: SVGContainerProps): JSX_2.Element;
|
||||
|
||||
|
@ -2281,6 +2290,7 @@ export interface TldrawEditorBaseProps {
|
|||
inferDarkMode?: boolean;
|
||||
initialState?: string;
|
||||
onMount?: TLOnMountHandler;
|
||||
options?: Partial<TldrawOptions>;
|
||||
shapeUtils?: readonly TLAnyShapeUtilConstructor[];
|
||||
tools?: readonly TLStateNodeConstructor[];
|
||||
user?: TLUser;
|
||||
|
@ -2299,6 +2309,62 @@ export type TldrawEditorProps = Expand<TldrawEditorBaseProps & ({
|
|||
store: TLStore | TLStoreWithStatus;
|
||||
})>;
|
||||
|
||||
// @public
|
||||
export interface TldrawOptions {
|
||||
// (undocumented)
|
||||
readonly adjacentShapeMargin: number;
|
||||
// (undocumented)
|
||||
readonly animationMediumMs: number;
|
||||
// (undocumented)
|
||||
readonly cameraMovingTimoutMs: number;
|
||||
// (undocumented)
|
||||
readonly cameraSlideFriction: number;
|
||||
// (undocumented)
|
||||
readonly coarseDragDistanceSquared: number;
|
||||
// (undocumented)
|
||||
readonly coarseHandleRadius: number;
|
||||
// (undocumented)
|
||||
readonly coarsePointerWidth: number;
|
||||
// (undocumented)
|
||||
readonly collaboratorCheckIntervalMs: number;
|
||||
// (undocumented)
|
||||
readonly collaboratorIdleTimeoutMs: number;
|
||||
// (undocumented)
|
||||
readonly collaboratorInactiveTimeoutMs: number;
|
||||
// (undocumented)
|
||||
readonly defaultSvgPadding: number;
|
||||
// (undocumented)
|
||||
readonly doubleClickDurationMs: number;
|
||||
// (undocumented)
|
||||
readonly dragDistanceSquared: number;
|
||||
// (undocumented)
|
||||
readonly edgeScrollDistance: number;
|
||||
// (undocumented)
|
||||
readonly edgeScrollSpeed: number;
|
||||
// (undocumented)
|
||||
readonly followChaseViewportSnap: number;
|
||||
// (undocumented)
|
||||
readonly gridSteps: readonly {
|
||||
readonly mid: number;
|
||||
readonly min: number;
|
||||
readonly step: number;
|
||||
}[];
|
||||
// (undocumented)
|
||||
readonly handleRadius: number;
|
||||
// (undocumented)
|
||||
readonly hitTestMargin: number;
|
||||
// (undocumented)
|
||||
readonly longPressDurationMs: number;
|
||||
// (undocumented)
|
||||
readonly maxPages: number;
|
||||
// (undocumented)
|
||||
readonly maxShapesPerPage: number;
|
||||
// (undocumented)
|
||||
readonly multiClickDurationMs: number;
|
||||
// (undocumented)
|
||||
readonly textShadowLod: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLEditorComponents = Partial<{
|
||||
[K in keyof BaseEditorComponents]: BaseEditorComponents[K] | null;
|
||||
|
@ -2312,6 +2378,8 @@ export interface TLEditorOptions {
|
|||
getContainer: () => HTMLElement;
|
||||
inferDarkMode?: boolean;
|
||||
initialState?: string;
|
||||
// (undocumented)
|
||||
options?: Partial<TldrawOptions>;
|
||||
shapeUtils: readonly TLShapeUtilConstructor<TLUnknownShape>[];
|
||||
store: TLStore;
|
||||
tools: readonly TLStateNodeConstructor[];
|
||||
|
|
|
@ -106,22 +106,7 @@ export {
|
|||
export { createTLUser } from './lib/config/createTLUser'
|
||||
export { type TLAnyBindingUtilConstructor } from './lib/config/defaultBindings'
|
||||
export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
|
||||
export {
|
||||
ANIMATION_MEDIUM_MS,
|
||||
ANIMATION_SHORT_MS,
|
||||
CAMERA_SLIDE_FRICTION,
|
||||
DEFAULT_ANIMATION_OPTIONS,
|
||||
DEFAULT_CAMERA_OPTIONS,
|
||||
DOUBLE_CLICK_DURATION,
|
||||
DRAG_DISTANCE,
|
||||
GRID_STEPS,
|
||||
HIT_TEST_MARGIN,
|
||||
MAX_PAGES,
|
||||
MAX_SHAPES_PER_PAGE,
|
||||
MULTI_CLICK_DURATION,
|
||||
SIDES,
|
||||
SVG_PADDING,
|
||||
} from './lib/constants'
|
||||
export { DEFAULT_ANIMATION_OPTIONS, DEFAULT_CAMERA_OPTIONS, SIDES } from './lib/constants'
|
||||
export { Editor, type TLEditorOptions, type TLResizeShapeOptions } from './lib/editor/Editor'
|
||||
export {
|
||||
BindingUnbindReason,
|
||||
|
@ -247,6 +232,7 @@ export { useSafeId } from './lib/hooks/useSafeId'
|
|||
export { useSelectionEvents } from './lib/hooks/useSelectionEvents'
|
||||
export { useTLStore } from './lib/hooks/useTLStore'
|
||||
export { useTransform } from './lib/hooks/useTransform'
|
||||
export { defaultTldrawOptions, type TldrawOptions } from './lib/options'
|
||||
export {
|
||||
Box,
|
||||
ROTATE_CORNER_TO_SELECTION_CORNER,
|
||||
|
|
|
@ -33,6 +33,7 @@ import { useEvent } from './hooks/useEvent'
|
|||
import { useForceUpdate } from './hooks/useForceUpdate'
|
||||
import { useLocalStore } from './hooks/useLocalStore'
|
||||
import { useZoomCss } from './hooks/useZoomCss'
|
||||
import { TldrawOptions } from './options'
|
||||
import { stopEventPropagation } from './utils/dom'
|
||||
import { TLStoreWithStatus } from './utils/sync/StoreWithStatus'
|
||||
|
||||
|
@ -124,6 +125,11 @@ export interface TldrawEditorBaseProps {
|
|||
* Camera options for the editor.
|
||||
*/
|
||||
cameraOptions?: Partial<TLCameraOptions>
|
||||
|
||||
/**
|
||||
* Options for the editor.
|
||||
*/
|
||||
options?: Partial<TldrawOptions>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -293,6 +299,7 @@ function TldrawEditorWithReadyStore({
|
|||
autoFocus = true,
|
||||
inferDarkMode,
|
||||
cameraOptions,
|
||||
options,
|
||||
}: Required<
|
||||
TldrawEditorProps & {
|
||||
store: TLStore
|
||||
|
@ -317,6 +324,7 @@ function TldrawEditorWithReadyStore({
|
|||
autoFocus: initialAutoFocus,
|
||||
inferDarkMode,
|
||||
cameraOptions,
|
||||
options,
|
||||
})
|
||||
setEditor(editor)
|
||||
|
||||
|
@ -334,6 +342,7 @@ function TldrawEditorWithReadyStore({
|
|||
initialAutoFocus,
|
||||
inferDarkMode,
|
||||
cameraOptions,
|
||||
options,
|
||||
])
|
||||
|
||||
const crashingError = useSyncExternalStore(
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import { track } from '@tldraw/state'
|
||||
import { TLInstancePresence } from '@tldraw/tlschema'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
COLLABORATOR_CHECK_INTERVAL,
|
||||
COLLABORATOR_IDLE_TIMEOUT,
|
||||
COLLABORATOR_INACTIVE_TIMEOUT,
|
||||
} from '../constants'
|
||||
import { Editor } from '../editor/Editor'
|
||||
import { useEditor } from '../hooks/useEditor'
|
||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
||||
import { usePeerIds } from '../hooks/usePeerIds'
|
||||
|
@ -29,7 +25,7 @@ const CollaboratorGuard = track(function CollaboratorGuard({
|
|||
}) {
|
||||
const editor = useEditor()
|
||||
const presence = usePresence(collaboratorId)
|
||||
const collaboratorState = useCollaboratorState(presence)
|
||||
const collaboratorState = useCollaboratorState(editor, presence)
|
||||
|
||||
if (!(presence && presence.currentPageId === editor.getCurrentPageId())) {
|
||||
// No need to render if we don't have a presence or if they're on a different page
|
||||
|
@ -153,28 +149,28 @@ const Collaborator = track(function Collaborator({
|
|||
)
|
||||
})
|
||||
|
||||
function getStateFromElapsedTime(elapsed: number) {
|
||||
return elapsed > COLLABORATOR_INACTIVE_TIMEOUT
|
||||
function getStateFromElapsedTime(editor: Editor, elapsed: number) {
|
||||
return elapsed > editor.options.collaboratorInactiveTimeoutMs
|
||||
? 'inactive'
|
||||
: elapsed > COLLABORATOR_IDLE_TIMEOUT
|
||||
: elapsed > editor.options.collaboratorIdleTimeoutMs
|
||||
? 'idle'
|
||||
: 'active'
|
||||
}
|
||||
|
||||
function useCollaboratorState(latestPresence: TLInstancePresence | null) {
|
||||
function useCollaboratorState(editor: Editor, latestPresence: TLInstancePresence | null) {
|
||||
const rLastActivityTimestamp = useRef(latestPresence?.lastActivityTimestamp ?? -1)
|
||||
|
||||
const [state, setState] = useState<'active' | 'idle' | 'inactive'>(() =>
|
||||
getStateFromElapsedTime(Date.now() - rLastActivityTimestamp.current)
|
||||
getStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setState(getStateFromElapsedTime(Date.now() - rLastActivityTimestamp.current))
|
||||
}, COLLABORATOR_CHECK_INTERVAL)
|
||||
setState(getStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current))
|
||||
}, editor.options.collaboratorCheckIntervalMs)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
}, [editor])
|
||||
|
||||
if (latestPresence) {
|
||||
// We can do this on every render, it's free and cheaper than an effect
|
||||
|
|
|
@ -3,7 +3,6 @@ import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
|||
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
|
||||
import classNames from 'classnames'
|
||||
import { Fragment, JSX, useEffect, useRef, useState } from 'react'
|
||||
import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS, TEXT_SHADOW_LOD } from '../../constants'
|
||||
import { useCanvasEvents } from '../../hooks/useCanvasEvents'
|
||||
import { useCoarsePointer } from '../../hooks/useCoarsePointer'
|
||||
import { useContainer } from '../../hooks/useContainer'
|
||||
|
@ -65,9 +64,9 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
// If we're below the lod distance for text shadows, turn them off
|
||||
if (
|
||||
rMemoizedStuff.current.allowTextOutline &&
|
||||
z < TEXT_SHADOW_LOD !== rMemoizedStuff.current.lodDisableTextOutline
|
||||
z < editor.options.textShadowLod !== rMemoizedStuff.current.lodDisableTextOutline
|
||||
) {
|
||||
const lodDisableTextOutline = z < TEXT_SHADOW_LOD
|
||||
const lodDisableTextOutline = z < editor.options.textShadowLod
|
||||
container.style.setProperty(
|
||||
'--tl-text-outline',
|
||||
lodDisableTextOutline
|
||||
|
@ -297,7 +296,8 @@ function HandlesWrapperInner({ shapeId }: { shapeId: TLShapeId }) {
|
|||
if (!handles) return null
|
||||
|
||||
const minDistBetweenVirtualHandlesAndRegularHandles =
|
||||
((isCoarse ? COARSE_HANDLE_RADIUS : HANDLE_RADIUS) / zoomLevel) * 2
|
||||
((isCoarse ? editor.options.coarseHandleRadius : editor.options.handleRadius) / zoomLevel) *
|
||||
2
|
||||
|
||||
return (
|
||||
handles
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { modulate } from '@tldraw/utils'
|
||||
import { GRID_STEPS } from '../../constants'
|
||||
import { useEditor } from '../../hooks/useEditor'
|
||||
|
||||
/** @public */
|
||||
export interface TLGridProps {
|
||||
|
@ -11,10 +11,12 @@ export interface TLGridProps {
|
|||
|
||||
/** @public */
|
||||
export function DefaultGrid({ x, y, z, size }: TLGridProps) {
|
||||
const editor = useEditor()
|
||||
const { gridSteps } = editor.options
|
||||
return (
|
||||
<svg className="tl-grid" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
{GRID_STEPS.map(({ min, mid, step }, i) => {
|
||||
{gridSteps.map(({ min, mid, step }, i) => {
|
||||
const s = step * size * z
|
||||
const xo = 0.5 + x * z
|
||||
const yo = 0.5 + y * z
|
||||
|
@ -35,7 +37,7 @@ export function DefaultGrid({ x, y, z, size }: TLGridProps) {
|
|||
)
|
||||
})}
|
||||
</defs>
|
||||
{GRID_STEPS.map(({ step }, i) => (
|
||||
{gridSteps.map(({ step }, i) => (
|
||||
<rect key={`grid-rect-${i}`} width="100%" height="100%" fill={`url(#grid-${step})`} />
|
||||
))}
|
||||
</svg>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
||||
import classNames from 'classnames'
|
||||
import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS, SIDES } from '../../constants'
|
||||
import { SIDES } from '../../constants'
|
||||
import { useEditor } from '../../hooks/useEditor'
|
||||
|
||||
/** @public */
|
||||
export interface TLHandleProps {
|
||||
|
@ -13,7 +14,8 @@ export interface TLHandleProps {
|
|||
|
||||
/** @public */
|
||||
export function DefaultHandle({ handle, isCoarse, className, zoom }: TLHandleProps) {
|
||||
const br = (isCoarse ? COARSE_HANDLE_RADIUS : HANDLE_RADIUS) / zoom
|
||||
const editor = useEditor()
|
||||
const br = (isCoarse ? editor.options.coarseHandleRadius : editor.options.handleRadius) / zoom
|
||||
|
||||
if (handle.type === 'clone') {
|
||||
// bouba
|
||||
|
|
|
@ -1,16 +1,6 @@
|
|||
import { TLCameraOptions } from './editor/types/misc-types'
|
||||
import { EASINGS } from './primitives/easings'
|
||||
|
||||
/** @internal */
|
||||
export const MAX_SHAPES_PER_PAGE = 4000
|
||||
/** @internal */
|
||||
export const MAX_PAGES = 40
|
||||
|
||||
/** @internal */
|
||||
export const ANIMATION_SHORT_MS = 80
|
||||
/** @internal */
|
||||
export const ANIMATION_MEDIUM_MS = 320
|
||||
|
||||
/** @internal */
|
||||
export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = {
|
||||
isLocked: false,
|
||||
|
@ -20,49 +10,12 @@ export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = {
|
|||
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const FOLLOW_CHASE_VIEWPORT_SNAP = 2
|
||||
|
||||
/** @internal */
|
||||
export const DOUBLE_CLICK_DURATION = 450
|
||||
/** @internal */
|
||||
export const MULTI_CLICK_DURATION = 200
|
||||
|
||||
/** @internal */
|
||||
export const COARSE_DRAG_DISTANCE = 36 // 6 squared
|
||||
|
||||
/** @internal */
|
||||
export const DRAG_DISTANCE = 16 // 4 squared
|
||||
|
||||
/** @internal */
|
||||
export const SVG_PADDING = 32
|
||||
|
||||
/** @internal */
|
||||
export const DEFAULT_ANIMATION_OPTIONS = {
|
||||
duration: 0,
|
||||
easing: EASINGS.easeInOutCubic,
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const CAMERA_SLIDE_FRICTION = 0.09
|
||||
|
||||
/** @public */
|
||||
export const GRID_STEPS = [
|
||||
{ min: -1, mid: 0.15, step: 64 },
|
||||
{ min: 0.05, mid: 0.375, step: 16 },
|
||||
{ min: 0.15, mid: 1, step: 4 },
|
||||
{ min: 0.7, mid: 2.5, step: 1 },
|
||||
]
|
||||
|
||||
/** @internal */
|
||||
export const COLLABORATOR_INACTIVE_TIMEOUT = 60000
|
||||
|
||||
/** @internal */
|
||||
export const COLLABORATOR_IDLE_TIMEOUT = 3000
|
||||
|
||||
/** @internal */
|
||||
export const COLLABORATOR_CHECK_INTERVAL = 1200
|
||||
|
||||
/**
|
||||
* Negative pointer ids are reserved for internal use.
|
||||
*
|
||||
|
@ -71,36 +24,9 @@ export const INTERNAL_POINTER_IDS = {
|
|||
CAMERA_MOVE: -10,
|
||||
} as const
|
||||
|
||||
/** @internal */
|
||||
export const CAMERA_MOVING_TIMEOUT = 64
|
||||
|
||||
/** @public */
|
||||
export const HIT_TEST_MARGIN = 8
|
||||
|
||||
/** @internal */
|
||||
export const EDGE_SCROLL_SPEED = 20
|
||||
|
||||
/** @internal */
|
||||
export const EDGE_SCROLL_DISTANCE = 8
|
||||
|
||||
/** @internal */
|
||||
export const COARSE_POINTER_WIDTH = 12
|
||||
|
||||
/** @internal */
|
||||
export const COARSE_HANDLE_RADIUS = 20
|
||||
|
||||
/** @internal */
|
||||
export const HANDLE_RADIUS = 12
|
||||
|
||||
/** @public */
|
||||
export const SIDES = ['top', 'right', 'bottom', 'left'] as const
|
||||
|
||||
/** @internal */
|
||||
export const LONG_PRESS_DURATION = 500
|
||||
|
||||
/** @internal */
|
||||
export const TEXT_SHADOW_LOD = 0.35
|
||||
|
||||
export const LEFT_MOUSE_BUTTON = 0
|
||||
export const RIGHT_MOUSE_BUTTON = 2
|
||||
export const MIDDLE_MOUSE_BUTTON = 1
|
||||
|
|
|
@ -83,26 +83,16 @@ import { TLUser, createTLUser } from '../config/createTLUser'
|
|||
import { checkBindings } from '../config/defaultBindings'
|
||||
import { checkShapesAndAddCore } from '../config/defaultShapes'
|
||||
import {
|
||||
ANIMATION_MEDIUM_MS,
|
||||
CAMERA_MOVING_TIMEOUT,
|
||||
CAMERA_SLIDE_FRICTION,
|
||||
COARSE_DRAG_DISTANCE,
|
||||
COLLABORATOR_IDLE_TIMEOUT,
|
||||
DEFAULT_ANIMATION_OPTIONS,
|
||||
DEFAULT_CAMERA_OPTIONS,
|
||||
DRAG_DISTANCE,
|
||||
FOLLOW_CHASE_VIEWPORT_SNAP,
|
||||
HIT_TEST_MARGIN,
|
||||
INTERNAL_POINTER_IDS,
|
||||
LEFT_MOUSE_BUTTON,
|
||||
LONG_PRESS_DURATION,
|
||||
MAX_PAGES,
|
||||
MAX_SHAPES_PER_PAGE,
|
||||
MIDDLE_MOUSE_BUTTON,
|
||||
RIGHT_MOUSE_BUTTON,
|
||||
STYLUS_ERASER_BUTTON,
|
||||
ZOOM_TO_FIT_PADDING,
|
||||
} from '../constants'
|
||||
import { TldrawOptions, defaultTldrawOptions } from '../options'
|
||||
import { Box, BoxLike } from '../primitives/Box'
|
||||
import { Mat, MatLike } from '../primitives/Mat'
|
||||
import { Vec, VecLike } from '../primitives/Vec'
|
||||
|
@ -217,6 +207,8 @@ export interface TLEditorOptions {
|
|||
* Options for the editor's camera.
|
||||
*/
|
||||
cameraOptions?: Partial<TLCameraOptions>
|
||||
|
||||
options?: Partial<TldrawOptions>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -232,9 +224,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
initialState,
|
||||
autoFocus,
|
||||
inferDarkMode,
|
||||
options,
|
||||
}: TLEditorOptions) {
|
||||
super()
|
||||
|
||||
this.options = { ...defaultTldrawOptions, ...options }
|
||||
this.store = store
|
||||
this.history = new HistoryManager<TLRecord>({
|
||||
store,
|
||||
|
@ -700,6 +694,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
this.performanceTracker = new PerformanceTracker()
|
||||
}
|
||||
|
||||
readonly options: TldrawOptions
|
||||
|
||||
/**
|
||||
* The editor's store
|
||||
*
|
||||
|
@ -2876,7 +2872,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
opts = {} as {
|
||||
speed: number
|
||||
direction: VecLike
|
||||
friction: number
|
||||
friction?: number
|
||||
speedThreshold?: number
|
||||
}
|
||||
): this {
|
||||
|
@ -2887,7 +2883,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
this.stopCameraAnimation()
|
||||
|
||||
const { speed, friction, direction, speedThreshold = 0.01 } = opts
|
||||
const {
|
||||
speed,
|
||||
friction = this.options.cameraSlideFriction,
|
||||
direction,
|
||||
speedThreshold = 0.01,
|
||||
} = opts
|
||||
let currentSpeed = Math.min(speed, 1)
|
||||
|
||||
const cancel = () => {
|
||||
|
@ -2963,7 +2964,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
if (index < 0) return
|
||||
highlightedUserIds.splice(index, 1)
|
||||
this.updateInstanceState({ highlightedUserIds })
|
||||
}, COLLABORATOR_IDLE_TIMEOUT)
|
||||
}, this.options.collaboratorIdleTimeoutMs)
|
||||
})
|
||||
|
||||
return this
|
||||
|
@ -3279,7 +3280,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
Math.abs(targetViewport.maxY - currentViewport.maxY)
|
||||
|
||||
// Stop chasing if we're close enough!
|
||||
if (diffX < FOLLOW_CHASE_VIEWPORT_SNAP && diffY < FOLLOW_CHASE_VIEWPORT_SNAP) {
|
||||
if (
|
||||
diffX < this.options.followChaseViewportSnap &&
|
||||
diffY < this.options.followChaseViewportSnap
|
||||
) {
|
||||
this._isLockedOnFollowingUser.set(true)
|
||||
return
|
||||
}
|
||||
|
@ -3364,8 +3368,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
opacity: number
|
||||
}[] = []
|
||||
|
||||
let nextIndex = MAX_SHAPES_PER_PAGE * 2
|
||||
let nextBackgroundIndex = MAX_SHAPES_PER_PAGE
|
||||
let nextIndex = this.options.maxShapesPerPage * 2
|
||||
let nextBackgroundIndex = this.options.maxShapesPerPage
|
||||
|
||||
const erasingShapeIds = this.getErasingShapeIds()
|
||||
|
||||
|
@ -3403,7 +3407,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
if (util.providesBackgroundForChildren(shape)) {
|
||||
backgroundIndexToRestore = nextBackgroundIndex
|
||||
nextBackgroundIndex = nextIndex
|
||||
nextIndex += MAX_SHAPES_PER_PAGE
|
||||
nextIndex += this.options.maxShapesPerPage
|
||||
}
|
||||
|
||||
for (const childId of childIds) {
|
||||
|
@ -3444,7 +3448,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
private _tickCameraState = () => {
|
||||
// always reset the timeout
|
||||
this._cameraStateTimeoutRemaining = CAMERA_MOVING_TIMEOUT
|
||||
this._cameraStateTimeoutRemaining = this.options.cameraMovingTimoutMs
|
||||
// If the state is idle, then start the tick
|
||||
if (this._cameraState.__unsafe__getWithoutCapture() !== 'idle') return
|
||||
this._cameraState.set('moving')
|
||||
|
@ -3667,7 +3671,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
createPage(page: Partial<TLPage>): this {
|
||||
this.history.batch(() => {
|
||||
if (this.getInstanceState().isReadonly) return
|
||||
if (this.getPages().length >= MAX_PAGES) return
|
||||
if (this.getPages().length >= this.options.maxPages) return
|
||||
const pages = this.getPages()
|
||||
|
||||
const name = getIncrementedName(
|
||||
|
@ -3734,7 +3738,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
duplicatePage(page: TLPageId | TLPage, createId: TLPageId = PageRecordType.createId()): this {
|
||||
if (this.getPages().length >= MAX_PAGES) return this
|
||||
if (this.getPages().length >= this.options.maxPages) return this
|
||||
const id = typeof page === 'string' ? page : page.id
|
||||
const freshPage = this.getPage(id) // get the most recent version of the page anyway
|
||||
if (!freshPage) return this
|
||||
|
@ -4543,7 +4547,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
} else {
|
||||
// For open shapes (e.g. lines or draw shapes) always use the margin.
|
||||
// If the distance is less than the margin, return the shape as the hit.
|
||||
if (distance < HIT_TEST_MARGIN / zoomLevel) {
|
||||
if (distance < this.options.hitTestMargin / zoomLevel) {
|
||||
return shape
|
||||
}
|
||||
}
|
||||
|
@ -5445,7 +5449,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
)
|
||||
|
||||
const maxShapesReached =
|
||||
shapesToCreate.length + this.getCurrentPageShapeIds().size > MAX_SHAPES_PER_PAGE
|
||||
shapesToCreate.length + this.getCurrentPageShapeIds().size > this.options.maxShapesPerPage
|
||||
|
||||
if (maxShapesReached) {
|
||||
alertMaxShapes(this)
|
||||
|
@ -5464,7 +5468,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const viewportPageBounds = this.getViewportPageBounds()
|
||||
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
|
||||
this.centerOnPoint(selectionPageBounds.center, {
|
||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||
animation: { duration: this.options.animationMediumMs },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -5508,7 +5512,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
// If there is no space on pageId, or if the selected shapes
|
||||
// would take the new page above the limit, don't move the shapes
|
||||
if (this.getPageShapeIds(pageId).size + content.shapes.length > MAX_SHAPES_PER_PAGE) {
|
||||
if (this.getPageShapeIds(pageId).size + content.shapes.length > this.options.maxShapesPerPage) {
|
||||
alertMaxShapes(this, pageId)
|
||||
return this
|
||||
}
|
||||
|
@ -6611,7 +6615,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
const currentPageShapeIds = this.getCurrentPageShapeIds()
|
||||
|
||||
const maxShapesReached = shapes.length + currentPageShapeIds.size > MAX_SHAPES_PER_PAGE
|
||||
const maxShapesReached =
|
||||
shapes.length + currentPageShapeIds.size > this.options.maxShapesPerPage
|
||||
|
||||
if (maxShapesReached) {
|
||||
// can't create more shapes than fit on the page
|
||||
|
@ -7855,7 +7860,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
return newShape
|
||||
})
|
||||
|
||||
if (newShapes.length + this.getCurrentPageShapeIds().size > MAX_SHAPES_PER_PAGE) {
|
||||
if (newShapes.length + this.getCurrentPageShapeIds().size > this.options.maxShapesPerPage) {
|
||||
// There's some complexity here involving children
|
||||
// that might be created without their parents, so
|
||||
// if we're going over the limit then just don't paste.
|
||||
|
@ -8613,7 +8618,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
point: this.inputs.currentScreenPoint,
|
||||
name: 'long_press',
|
||||
})
|
||||
}, LONG_PRESS_DURATION)
|
||||
}, this.options.longPressDurationMs)
|
||||
}
|
||||
|
||||
// Save the selected ids at pointer down
|
||||
|
@ -8681,7 +8686,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
inputs.isPointing &&
|
||||
!inputs.isDragging &&
|
||||
Vec.Dist2(originPagePoint, currentPagePoint) >
|
||||
(instanceState.isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) / cz
|
||||
(instanceState.isCoarsePointer
|
||||
? this.options.coarseDragDistanceSquared
|
||||
: this.options.dragDistanceSquared) /
|
||||
cz
|
||||
) {
|
||||
// Start dragging
|
||||
inputs.isDragging = true
|
||||
|
@ -8734,11 +8742,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
|
||||
if (slideSpeed > 0) {
|
||||
this.slideCamera({
|
||||
speed: slideSpeed,
|
||||
direction: slideDirection,
|
||||
friction: CAMERA_SLIDE_FRICTION,
|
||||
})
|
||||
this.slideCamera({ speed: slideSpeed, direction: slideDirection })
|
||||
}
|
||||
} else {
|
||||
if (info.button === STYLUS_ERASER_BUTTON) {
|
||||
|
@ -8851,7 +8855,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
function alertMaxShapes(editor: Editor, pageId = editor.getCurrentPageId()) {
|
||||
const name = editor.getPage(pageId)!.name
|
||||
editor.emit('max-shapes', { name, pageId, count: MAX_SHAPES_PER_PAGE })
|
||||
editor.emit('max-shapes', { name, pageId, count: editor.options.maxShapesPerPage })
|
||||
}
|
||||
|
||||
function applyPartialToRecordWithProps<
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
getDefaultColorTheme,
|
||||
} from '@tldraw/tlschema'
|
||||
import { Fragment, ReactElement } from 'react'
|
||||
import { SVG_PADDING } from '../constants'
|
||||
import { Editor } from './Editor'
|
||||
import { SvgExportContext, SvgExportContextProvider, SvgExportDef } from './types/SvgExportContext'
|
||||
import { TLSvgOptions } from './types/misc-types'
|
||||
|
@ -22,7 +21,12 @@ export async function getSvgJsx(
|
|||
if (ids.length === 0) return
|
||||
if (!window.document) throw Error('No document')
|
||||
|
||||
const { scale = 1, background = false, padding = SVG_PADDING, preserveAspectRatio = false } = opts
|
||||
const {
|
||||
scale = 1,
|
||||
background = false,
|
||||
padding = editor.options.defaultSvgPadding,
|
||||
preserveAspectRatio = false,
|
||||
} = opts
|
||||
|
||||
const isDarkMode = opts.darkMode ?? editor.user.getIsDarkMode()
|
||||
const theme = getDefaultColorTheme({ isDarkMode })
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
import {
|
||||
COARSE_DRAG_DISTANCE,
|
||||
DOUBLE_CLICK_DURATION,
|
||||
DRAG_DISTANCE,
|
||||
MULTI_CLICK_DURATION,
|
||||
} from '../../constants'
|
||||
import { Vec } from '../../primitives/Vec'
|
||||
import { uniqueId } from '../../utils/uniqueId'
|
||||
import type { Editor } from '../Editor'
|
||||
|
@ -72,7 +66,9 @@ export class ClickManager {
|
|||
this._clickState = 'idle'
|
||||
}
|
||||
},
|
||||
state === 'idle' || state === 'pendingDouble' ? DOUBLE_CLICK_DURATION : MULTI_CLICK_DURATION
|
||||
state === 'idle' || state === 'pendingDouble'
|
||||
? this.editor.options.doubleClickDurationMs
|
||||
: this.editor.options.multiClickDurationMs
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -199,7 +195,9 @@ export class ClickManager {
|
|||
this._clickState !== 'idle' &&
|
||||
this._clickScreenPoint &&
|
||||
Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) >
|
||||
(this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
|
||||
(this.editor.getInstanceState().isCoarsePointer
|
||||
? this.editor.options.coarseDragDistanceSquared
|
||||
: this.editor.options.dragDistanceSquared)
|
||||
) {
|
||||
this.cancelDoubleClickTimeout()
|
||||
}
|
||||
|
|
80
packages/editor/src/lib/options.ts
Normal file
80
packages/editor/src/lib/options.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const options: Partial<TldrawOptions> = {
|
||||
* maxPages: 3,
|
||||
* maxShapesPerPage: 1000,
|
||||
* }
|
||||
*
|
||||
* function MyTldrawComponent() {
|
||||
* return <Tldraw options={options} />
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface TldrawOptions {
|
||||
readonly maxShapesPerPage: number
|
||||
readonly maxPages: number
|
||||
readonly animationMediumMs: number
|
||||
readonly followChaseViewportSnap: number
|
||||
readonly doubleClickDurationMs: number
|
||||
readonly multiClickDurationMs: number
|
||||
readonly coarseDragDistanceSquared: number
|
||||
readonly dragDistanceSquared: number
|
||||
readonly defaultSvgPadding: number
|
||||
readonly cameraSlideFriction: number
|
||||
readonly gridSteps: readonly {
|
||||
readonly min: number
|
||||
readonly mid: number
|
||||
readonly step: number
|
||||
}[]
|
||||
readonly collaboratorInactiveTimeoutMs: number
|
||||
readonly collaboratorIdleTimeoutMs: number
|
||||
readonly collaboratorCheckIntervalMs: number
|
||||
readonly cameraMovingTimoutMs: number
|
||||
readonly hitTestMargin: number
|
||||
readonly edgeScrollSpeed: number
|
||||
readonly edgeScrollDistance: number
|
||||
readonly coarsePointerWidth: number
|
||||
readonly coarseHandleRadius: number
|
||||
readonly handleRadius: number
|
||||
readonly longPressDurationMs: number
|
||||
readonly textShadowLod: number
|
||||
readonly adjacentShapeMargin: number
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const defaultTldrawOptions = {
|
||||
maxShapesPerPage: 4000,
|
||||
maxPages: 40,
|
||||
animationMediumMs: 320,
|
||||
followChaseViewportSnap: 2,
|
||||
doubleClickDurationMs: 450,
|
||||
multiClickDurationMs: 200,
|
||||
coarseDragDistanceSquared: 36, // 6 squared
|
||||
dragDistanceSquared: 16, // 4 squared
|
||||
defaultSvgPadding: 32,
|
||||
cameraSlideFriction: 0.09,
|
||||
gridSteps: [
|
||||
{ min: -1, mid: 0.15, step: 64 },
|
||||
{ min: 0.05, mid: 0.375, step: 16 },
|
||||
{ min: 0.15, mid: 1, step: 4 },
|
||||
{ min: 0.7, mid: 2.5, step: 1 },
|
||||
],
|
||||
collaboratorInactiveTimeoutMs: 60000,
|
||||
collaboratorIdleTimeoutMs: 3000,
|
||||
collaboratorCheckIntervalMs: 1200,
|
||||
cameraMovingTimoutMs: 64,
|
||||
hitTestMargin: 8,
|
||||
edgeScrollSpeed: 20,
|
||||
edgeScrollDistance: 8,
|
||||
coarsePointerWidth: 12,
|
||||
coarseHandleRadius: 20,
|
||||
handleRadius: 12,
|
||||
longPressDurationMs: 500,
|
||||
textShadowLod: 0.35,
|
||||
adjacentShapeMargin: 10,
|
||||
} as const satisfies TldrawOptions
|
|
@ -1,4 +1,3 @@
|
|||
import { COARSE_POINTER_WIDTH, EDGE_SCROLL_DISTANCE, EDGE_SCROLL_SPEED } from '../constants'
|
||||
import { Editor } from '../editor/Editor'
|
||||
import { Vec } from '../primitives/Vec'
|
||||
|
||||
|
@ -9,14 +8,15 @@ import { Vec } from '../primitives/Vec'
|
|||
* @internal
|
||||
*/
|
||||
function getEdgeProximityFactor(
|
||||
editor: Editor,
|
||||
position: number,
|
||||
dimension: number,
|
||||
isCoarse: boolean,
|
||||
insetStart: boolean,
|
||||
insetEnd: boolean
|
||||
) {
|
||||
const dist = EDGE_SCROLL_DISTANCE
|
||||
const pw = isCoarse ? COARSE_POINTER_WIDTH : 0 // pointer width
|
||||
const dist = editor.options.edgeScrollDistance
|
||||
const pw = isCoarse ? editor.options.coarsePointerWidth : 0 // pointer width
|
||||
const pMin = position - pw
|
||||
const pMax = position + pw
|
||||
const min = insetStart ? 0 : dist
|
||||
|
@ -53,13 +53,13 @@ export function moveCameraWhenCloseToEdge(editor: Editor) {
|
|||
isCoarsePointer,
|
||||
insets: [t, r, b, l],
|
||||
} = editor.getInstanceState()
|
||||
const proximityFactorX = getEdgeProximityFactor(x, screenBounds.w, isCoarsePointer, l, r)
|
||||
const proximityFactorY = getEdgeProximityFactor(y, screenBounds.h, isCoarsePointer, t, b)
|
||||
const proximityFactorX = getEdgeProximityFactor(editor, x, screenBounds.w, isCoarsePointer, l, r)
|
||||
const proximityFactorY = getEdgeProximityFactor(editor, y, screenBounds.h, isCoarsePointer, t, b)
|
||||
|
||||
if (proximityFactorX === 0 && proximityFactorY === 0) return
|
||||
|
||||
// Determines the base speed of the scroll
|
||||
const pxSpeed = editor.user.getEdgeScrollSpeed() * EDGE_SCROLL_SPEED
|
||||
const pxSpeed = editor.user.getEdgeScrollSpeed() * editor.options.edgeScrollSpeed
|
||||
const scrollDeltaX = (pxSpeed * proximityFactorX * screenSizeFactorX) / zoomLevel
|
||||
const scrollDeltaY = (pxSpeed * proximityFactorY * screenSizeFactorY) / zoomLevel
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
DRAG_DISTANCE,
|
||||
Mat,
|
||||
StateNode,
|
||||
TLDefaultSizeStyle,
|
||||
|
@ -317,7 +316,8 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
|
||||
const hasMovedFarEnough =
|
||||
Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
|
||||
Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) >
|
||||
this.editor.options.dragDistanceSquared
|
||||
|
||||
// Find the distance from where the pointer was when shift was released and
|
||||
// where it is now; if it's far enough away, then update the page point where
|
||||
|
@ -388,7 +388,8 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
|
||||
const hasMovedFarEnough =
|
||||
Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
|
||||
Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) >
|
||||
this.editor.options.dragDistanceSquared
|
||||
|
||||
// Find the distance from where the pointer was when shift was released and
|
||||
// where it is now; if it's far enough away, then update the page point where
|
||||
|
|
|
@ -36,7 +36,6 @@ import {
|
|||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||
|
||||
import { startEditingShapeWithLabel } from '../../tools/SelectTool/selectHelpers'
|
||||
import { ADJACENT_SHAPE_MARGIN } from '../../ui/constants'
|
||||
import { useForceSolid } from '../shared/useForceSolid'
|
||||
import {
|
||||
CLONE_HANDLE_MARGIN,
|
||||
|
@ -402,7 +401,7 @@ function useNoteKeydownHandler(id: TLShapeId) {
|
|||
|
||||
const offsetLength =
|
||||
NOTE_SIZE +
|
||||
ADJACENT_SHAPE_MARGIN +
|
||||
editor.options.adjacentShapeMargin +
|
||||
// If we're growing down, we need to account for the current shape's growY
|
||||
(isCmdEnter && !e.shiftKey ? shape.props.growY : 0)
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
HIT_TEST_MARGIN,
|
||||
StateNode,
|
||||
TLEventHandlers,
|
||||
TLFrameShape,
|
||||
|
@ -90,7 +89,7 @@ export class Erasing extends StateNode {
|
|||
this.pushPointToScribble()
|
||||
|
||||
const erasing = new Set<TLShapeId>(erasingShapeIds)
|
||||
const minDist = HIT_TEST_MARGIN / zoomLevel
|
||||
const minDist = this.editor.options.hitTestMargin / zoomLevel
|
||||
|
||||
for (const shape of currentPageShapes) {
|
||||
if (editor.isShapeOfType<TLGroupShape>(shape, 'group')) continue
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
HIT_TEST_MARGIN,
|
||||
StateNode,
|
||||
TLEventHandlers,
|
||||
TLFrameShape,
|
||||
|
@ -34,7 +33,7 @@ export class Pointing extends StateNode {
|
|||
if (
|
||||
this.editor.isPointInShape(shape, currentPagePoint, {
|
||||
hitInside: false,
|
||||
margin: HIT_TEST_MARGIN / zoomLevel,
|
||||
margin: this.editor.options.hitTestMargin / zoomLevel,
|
||||
})
|
||||
) {
|
||||
const hitShape = this.editor.getOutermostSelectableShape(shape)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CAMERA_SLIDE_FRICTION, StateNode, TLEventHandlers, Vec } from '@tldraw/editor'
|
||||
import { StateNode, TLEventHandlers, Vec } from '@tldraw/editor'
|
||||
|
||||
export class Dragging extends StateNode {
|
||||
static override id = 'dragging'
|
||||
|
@ -42,11 +42,7 @@ export class Dragging extends StateNode {
|
|||
const velocityAtPointerUp = Math.min(pointerVelocity.len(), 2)
|
||||
|
||||
if (velocityAtPointerUp > 0.1) {
|
||||
this.editor.slideCamera({
|
||||
speed: velocityAtPointerUp,
|
||||
direction: pointerVelocity,
|
||||
friction: CAMERA_SLIDE_FRICTION,
|
||||
})
|
||||
this.editor.slideCamera({ speed: velocityAtPointerUp, direction: pointerVelocity })
|
||||
}
|
||||
|
||||
this.parent.transition('idle')
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
Editor,
|
||||
Group2d,
|
||||
HIT_TEST_MARGIN,
|
||||
StateNode,
|
||||
TLArrowShape,
|
||||
TLClickEventInfo,
|
||||
|
@ -199,7 +198,7 @@ export class Idle extends StateNode {
|
|||
? hoveredShape
|
||||
: this.editor.getSelectedShapeAtPoint(this.editor.inputs.currentPagePoint) ??
|
||||
this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, {
|
||||
margin: HIT_TEST_MARGIN / this.editor.getZoomLevel(),
|
||||
margin: this.editor.options.hitTestMargin / this.editor.getZoomLevel(),
|
||||
hitInside: false,
|
||||
})
|
||||
|
||||
|
@ -353,7 +352,7 @@ export class Idle extends StateNode {
|
|||
hoveredShape && !this.editor.isShapeOfType<TLGroupShape>(hoveredShape, 'group')
|
||||
? hoveredShape
|
||||
: this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, {
|
||||
margin: HIT_TEST_MARGIN / this.editor.getZoomLevel(),
|
||||
margin: this.editor.options.hitTestMargin / this.editor.getZoomLevel(),
|
||||
hitInside: false,
|
||||
hitLabels: true,
|
||||
hitLocked: true,
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
HIT_TEST_MARGIN,
|
||||
StateNode,
|
||||
TLEventHandlers,
|
||||
TLPointerEventInfo,
|
||||
TLShape,
|
||||
} from '@tldraw/editor'
|
||||
import { StateNode, TLEventHandlers, TLPointerEventInfo, TLShape } from '@tldraw/editor'
|
||||
import { getTextLabels } from '../../../utils/shapes/shapes'
|
||||
|
||||
export class PointingShape extends StateNode {
|
||||
|
@ -73,7 +67,7 @@ export class PointingShape extends StateNode {
|
|||
|
||||
const hitShape =
|
||||
this.editor.getShapeAtPoint(currentPagePoint, {
|
||||
margin: HIT_TEST_MARGIN / zoomLevel,
|
||||
margin: this.editor.options.hitTestMargin / zoomLevel,
|
||||
hitInside: true,
|
||||
renderingOnly: true,
|
||||
}) ?? this.hitShape
|
||||
|
|
|
@ -98,7 +98,7 @@ export class ScribbleBrushing extends StateNode {
|
|||
const shapes = currentPageShapes
|
||||
let shape: TLShape, geometry: Geometry2d, A: Vec, B: Vec
|
||||
|
||||
const minDist = 0 // HIT_TEST_MARGIN / zoomLevel
|
||||
const minDist = 0 // this.editor.options.hitTestMargin / zoomLevel
|
||||
|
||||
for (let i = 0, n = shapes.length; i < n; i++) {
|
||||
shape = shapes[i]
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
ANIMATION_MEDIUM_MS,
|
||||
Editor,
|
||||
Geometry2d,
|
||||
Mat,
|
||||
|
@ -149,7 +148,7 @@ export function zoomToShapeIfOffscreen(editor: Editor) {
|
|||
})
|
||||
editor.zoomToBounds(nextBounds, {
|
||||
animation: {
|
||||
duration: ANIMATION_MEDIUM_MS,
|
||||
duration: editor.options.animationMediumMs,
|
||||
},
|
||||
inset: 0,
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Editor, HIT_TEST_MARGIN, TLShape } from '@tldraw/editor'
|
||||
import { Editor, TLShape } from '@tldraw/editor'
|
||||
|
||||
export function getHitShapeOnCanvasPointerDown(
|
||||
editor: Editor,
|
||||
|
@ -14,7 +14,7 @@ export function getHitShapeOnCanvasPointerDown(
|
|||
editor.getShapeAtPoint(currentPagePoint, {
|
||||
hitInside: false,
|
||||
hitLabels,
|
||||
margin: HIT_TEST_MARGIN / zoomLevel,
|
||||
margin: editor.options.hitTestMargin / zoomLevel,
|
||||
renderingOnly: true,
|
||||
}) ??
|
||||
// selected shape at point
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Editor, HIT_TEST_MARGIN, TLShape, isShapeId } from '@tldraw/editor'
|
||||
import { Editor, TLShape, isShapeId } from '@tldraw/editor'
|
||||
|
||||
export function selectOnCanvasPointerUp(editor: Editor) {
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
|
@ -6,7 +6,7 @@ export function selectOnCanvasPointerUp(editor: Editor) {
|
|||
|
||||
const hitShape = editor.getShapeAtPoint(currentPagePoint, {
|
||||
hitInside: false,
|
||||
margin: HIT_TEST_MARGIN / editor.getZoomLevel(),
|
||||
margin: editor.options.hitTestMargin / editor.getZoomLevel(),
|
||||
hitLabels: true,
|
||||
renderingOnly: true,
|
||||
filter: (shape) => !shape.isLocked,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Editor, HIT_TEST_MARGIN, TLShape, throttle } from '@tldraw/editor'
|
||||
import { Editor, TLShape, throttle } from '@tldraw/editor'
|
||||
|
||||
function _updateHoveredShapeId(editor: Editor) {
|
||||
// todo: consider replacing `get hoveredShapeId` with this; it would mean keeping hoveredShapeId in memory rather than in the store and possibly re-computing it more often than necessary
|
||||
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint, {
|
||||
hitInside: false,
|
||||
hitLabels: false,
|
||||
margin: HIT_TEST_MARGIN / editor.getZoomLevel(),
|
||||
margin: editor.options.hitTestMargin / editor.getZoomLevel(),
|
||||
renderingOnly: true,
|
||||
})
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
ANIMATION_MEDIUM_MS,
|
||||
Box,
|
||||
TLPointerEventInfo,
|
||||
Vec,
|
||||
|
@ -62,7 +61,7 @@ export function DefaultMinimap() {
|
|||
minimapRef.current.originPagePoint.setTo(clampedPoint)
|
||||
minimapRef.current.originPageCenter.setTo(editor.getViewportPageBounds().center)
|
||||
|
||||
editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } })
|
||||
editor.centerOnPoint(point, { animation: { duration: editor.options.animationMediumMs } })
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
@ -101,7 +100,7 @@ export function DefaultMinimap() {
|
|||
const pagePoint = Vec.Add(point, delta)
|
||||
minimapRef.current.originPagePoint.setTo(pagePoint)
|
||||
minimapRef.current.originPageCenter.setTo(point)
|
||||
editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } })
|
||||
editor.centerOnPoint(point, { animation: { duration: editor.options.animationMediumMs } })
|
||||
} else {
|
||||
const clampedPoint = minimapRef.current.minimapScreenPointToPagePoint(
|
||||
e.clientX,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
MAX_PAGES,
|
||||
PageRecordType,
|
||||
TLPageId,
|
||||
releasePointerCapture,
|
||||
|
@ -50,7 +49,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
|
|||
// If the user has reached the max page count, we disable the "add page" button
|
||||
const maxPageCountReached = useValue(
|
||||
'maxPageCountReached',
|
||||
() => editor.getPages().length >= MAX_PAGES,
|
||||
() => editor.getPages().length >= editor.options.maxPages,
|
||||
[editor]
|
||||
)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { MAX_PAGES, PageRecordType, TLPageId, track, useEditor } from '@tldraw/editor'
|
||||
import { PageRecordType, TLPageId, track, useEditor } from '@tldraw/editor'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||
import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
|
||||
|
@ -66,7 +66,7 @@ export const PageItemSubmenu = track(function PageItemSubmenu({
|
|||
id="duplicate"
|
||||
label="page-menu.submenu.duplicate-page"
|
||||
onSelect={onDuplicate}
|
||||
disabled={pages.length >= MAX_PAGES}
|
||||
disabled={pages.length >= editor.options.maxPages}
|
||||
/>
|
||||
{index > 0 && (
|
||||
<TldrawUiMenuItem
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as _Dropdown from '@radix-ui/react-dropdown-menu'
|
||||
import { ANIMATION_MEDIUM_MS, useContainer, useEditor, useValue } from '@tldraw/editor'
|
||||
import { useContainer, useEditor, useValue } from '@tldraw/editor'
|
||||
import { ReactNode, forwardRef, memo, useCallback } from 'react'
|
||||
import { PORTRAIT_BREAKPOINT } from '../../constants'
|
||||
import { useBreakpoint } from '../../context/breakpoints'
|
||||
|
@ -56,7 +56,7 @@ const ZoomTriggerButton = forwardRef<HTMLButtonElement, any>(
|
|||
|
||||
const handleDoubleClick = useCallback(() => {
|
||||
editor.resetZoom(editor.getViewportScreenCenter(), {
|
||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||
animation: { duration: editor.options.animationMediumMs },
|
||||
})
|
||||
}, [editor])
|
||||
|
||||
|
|
|
@ -13,5 +13,3 @@ export enum PORTRAIT_BREAKPOINT {
|
|||
TABLET = 6,
|
||||
DESKTOP = 7,
|
||||
}
|
||||
|
||||
export const ADJACENT_SHAPE_MARGIN = 20
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
ANIMATION_MEDIUM_MS,
|
||||
Box,
|
||||
DefaultColorStyle,
|
||||
Editor,
|
||||
|
@ -25,7 +24,6 @@ import { getEmbedInfo } from '../../utils/embeds/embeds'
|
|||
import { fitFrameToContent, removeFrame } from '../../utils/frames/frames'
|
||||
import { EditLinkDialog } from '../components/EditLinkDialog'
|
||||
import { EmbedDialog } from '../components/EmbedDialog'
|
||||
import { ADJACENT_SHAPE_MARGIN } from '../constants'
|
||||
import { useMenuClipboardEvents } from '../hooks/useClipboardEvents'
|
||||
import { useCopyAs } from '../hooks/useCopyAs'
|
||||
import { useExportAs } from '../hooks/useExportAs'
|
||||
|
@ -819,7 +817,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
trackEvent('pack-shapes', { source })
|
||||
editor.mark('pack')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.packShapes(selectedShapeIds, ADJACENT_SHAPE_MARGIN)
|
||||
editor.packShapes(selectedShapeIds, editor.options.adjacentShapeMargin)
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
|
@ -1038,7 +1036,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
onSelect(source) {
|
||||
trackEvent('zoom-in', { source })
|
||||
editor.zoomIn(undefined, {
|
||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||
animation: { duration: editor.options.animationMediumMs },
|
||||
})
|
||||
},
|
||||
},
|
||||
|
@ -1050,7 +1048,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
onSelect(source) {
|
||||
trackEvent('zoom-out', { source })
|
||||
editor.zoomOut(undefined, {
|
||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||
animation: { duration: editor.options.animationMediumMs },
|
||||
})
|
||||
},
|
||||
},
|
||||
|
@ -1063,7 +1061,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
onSelect(source) {
|
||||
trackEvent('reset-zoom', { source })
|
||||
editor.resetZoom(undefined, {
|
||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||
animation: { duration: editor.options.animationMediumMs },
|
||||
})
|
||||
},
|
||||
},
|
||||
|
@ -1074,7 +1072,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
readonlyOk: true,
|
||||
onSelect(source) {
|
||||
trackEvent('zoom-to-fit', { source })
|
||||
editor.zoomToFit({ animation: { duration: ANIMATION_MEDIUM_MS } })
|
||||
editor.zoomToFit({ animation: { duration: editor.options.animationMediumMs } })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1087,7 +1085,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('zoom-to-selection', { source })
|
||||
editor.zoomToSelection({ animation: { duration: ANIMATION_MEDIUM_MS } })
|
||||
editor.zoomToSelection({ animation: { duration: editor.options.animationMediumMs } })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
AssetRecordType,
|
||||
Editor,
|
||||
MAX_SHAPES_PER_PAGE,
|
||||
PageRecordType,
|
||||
TLArrowShape,
|
||||
TLArrowShapeArrowheadStyle,
|
||||
|
@ -130,7 +129,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
|
|||
|
||||
const v1Shapes = Object.values(v1Page.shapes ?? {})
|
||||
.sort((a, b) => (a.childIndex < b.childIndex ? -1 : 1))
|
||||
.slice(0, MAX_SHAPES_PER_PAGE)
|
||||
.slice(0, editor.options.maxShapesPerPage)
|
||||
|
||||
// Groups only
|
||||
v1Shapes.forEach((v1Shape) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { MAX_PAGES, PageRecordType } from '@tldraw/editor'
|
||||
import { PageRecordType } from '@tldraw/editor'
|
||||
import { TestEditor } from '../TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
@ -31,10 +31,10 @@ it('Creates a page', () => {
|
|||
})
|
||||
|
||||
it("Doesn't create a page if max pages is reached", () => {
|
||||
for (let i = 0; i < MAX_PAGES + 1; i++) {
|
||||
for (let i = 0; i < editor.options.maxPages + 1; i++) {
|
||||
editor.createPage({ name: `Test Page ${i}` })
|
||||
}
|
||||
expect(editor.getPages().length).toBe(MAX_PAGES)
|
||||
expect(editor.getPages().length).toBe(editor.options.maxPages)
|
||||
})
|
||||
|
||||
it('[regression] does not die if every page has the same index', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { MAX_PAGES, createShapeId } from '@tldraw/editor'
|
||||
import { createShapeId } from '@tldraw/editor'
|
||||
import { TestEditor } from '../TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
@ -48,8 +48,8 @@ it('Duplicates a page', () => {
|
|||
})
|
||||
|
||||
it("Doesn't duplicate the page if max pages is reached", () => {
|
||||
for (let i = 0; i < MAX_PAGES; i++) {
|
||||
for (let i = 0; i < editor.options.maxPages; i++) {
|
||||
editor.duplicatePage(editor.getCurrentPageId())
|
||||
}
|
||||
expect(editor.getPages().length).toBe(MAX_PAGES)
|
||||
expect(editor.getPages().length).toBe(editor.options.maxPages)
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { DefaultDashStyle, SVG_PADDING, createShapeId } from '@tldraw/editor'
|
||||
import { DefaultDashStyle, createShapeId } from '@tldraw/editor'
|
||||
import { TestEditor } from '../TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
@ -73,7 +73,7 @@ it('Gets the bounding box at the correct size', async () => {
|
|||
const svg = await editor.getSvgString(editor.getSelectedShapeIds())
|
||||
const parsed = parseSvg(svg!)
|
||||
const bbox = editor.getSelectionRotatedPageBounds()!
|
||||
const expanded = bbox.expandBy(SVG_PADDING) // adds 32px padding
|
||||
const expanded = bbox.expandBy(editor.options.defaultSvgPadding) // adds 32px padding
|
||||
|
||||
expect(parsed.getAttribute('width')).toMatch(expanded.width + '')
|
||||
expect(parsed.getAttribute('height')).toMatch(expanded.height + '')
|
||||
|
|
Loading…
Reference in a new issue