[Snapping 1/5] Validation & strict types for fractional indexes (#2827)

Currently, we type our fractional index keys as `string` and don't have
any validation for them. I'm touching some of this code for my work on
line handles and wanted to change that:
- fractional indexes are now `IndexKey`s, not `string`s. `IndexKey`s
have a brand property so can't be used interchangeably with strings
(like our IDs)
- There's a new `T.indexKey` validator which we can use in our
validations to make sure we don't end up with nonsense keys.

This PR is part of a series - please don't merge it until the things
before it have landed!
1. #2827 (you are here)
2. #2831
3. #2793
4. #2841
5. #2845

### Change Type

- [x] `patch` — Bug fix

### Test Plan

1. Mostly relying on unit & end to end tests here - no user facing
changes.

- [x] Unit Tests
This commit is contained in:
alex 2024-02-14 17:53:30 +00:00 committed by GitHub
parent fb00358a53
commit 93c2ed615c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 989 additions and 681 deletions

View file

@ -13,6 +13,7 @@ import {
TLOnHandleDragHandler, TLOnHandleDragHandler,
TLOnResizeHandler, TLOnResizeHandler,
Vec, Vec,
ZERO_INDEX_KEY,
deepCopy, deepCopy,
getDefaultColorTheme, getDefaultColorTheme,
resizeBox, resizeBox,
@ -80,7 +81,7 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
type: 'vertex', type: 'vertex',
canBind: true, canBind: true,
canSnap: true, canSnap: true,
index: 'a1', index: ZERO_INDEX_KEY,
x: 0.5, x: 0.5,
y: 1.5, y: 1.5,
}, },

View file

@ -17,6 +17,7 @@ import { EMPTY_ARRAY } from '@tldraw/state';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import { HistoryEntry } from '@tldraw/store'; import { HistoryEntry } from '@tldraw/store';
import { HTMLProps } from 'react'; import { HTMLProps } from 'react';
import { IndexKey } from '@tldraw/utils';
import { JsonObject } from '@tldraw/utils'; import { JsonObject } from '@tldraw/utils';
import { JSX as JSX_2 } from 'react/jsx-runtime'; import { JSX as JSX_2 } from 'react/jsx-runtime';
import { MemoExoticComponent } from 'react'; import { MemoExoticComponent } from 'react';
@ -697,7 +698,7 @@ export class Editor extends EventEmitter<TLEventMap> {
getErasingShapes(): NonNullable<TLShape | undefined>[]; getErasingShapes(): NonNullable<TLShape | undefined>[];
getFocusedGroup(): TLShape | undefined; getFocusedGroup(): TLShape | undefined;
getFocusedGroupId(): TLPageId | TLShapeId; getFocusedGroupId(): TLPageId | TLShapeId;
getHighestIndexForParent(parent: TLPage | TLParentId | TLShape): string; getHighestIndexForParent(parent: TLPage | TLParentId | TLShape): IndexKey;
getHintingShape(): NonNullable<TLShape | undefined>[]; getHintingShape(): NonNullable<TLShape | undefined>[];
getHintingShapeIds(): TLShapeId[]; getHintingShapeIds(): TLShapeId[];
getHoveredShape(): TLShape | undefined; getHoveredShape(): TLShape | undefined;
@ -844,7 +845,7 @@ export class Editor extends EventEmitter<TLEventMap> {
} : TLExternalContent) => void) | null): this; } : TLExternalContent) => void) | null): this;
renamePage(page: TLPage | TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this; renamePage(page: TLPage | TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this;
renderingBoundsMargin: number; renderingBoundsMargin: number;
reparentShapes(shapes: TLShape[] | TLShapeId[], parentId: TLParentId, insertIndex?: string): this; reparentShapes(shapes: TLShape[] | TLShapeId[], parentId: TLParentId, insertIndex?: IndexKey): this;
resetZoom(point?: Vec, animation?: TLAnimationOptions): this; resetZoom(point?: Vec, animation?: TLAnimationOptions): this;
resizeShape(shape: TLShape | TLShapeId, scale: VecLike, options?: TLResizeShapeOptions): this; resizeShape(shape: TLShape | TLShapeId, scale: VecLike, options?: TLResizeShapeOptions): this;
readonly root: RootState; readonly root: RootState;
@ -1065,27 +1066,6 @@ export function getFreshUserPreferences(): TLUserPreferences;
// @public // @public
export function getIncrementedName(name: string, others: string[]): string; export function getIncrementedName(name: string, others: string[]): string;
// @public
export function getIndexAbove(below: string): string;
// @public
export function getIndexBelow(above: string): string;
// @public
export function getIndexBetween(below: string, above?: string): string;
// @public
export function getIndices(n: number, start?: string): string[];
// @public
export function getIndicesAbove(below: string, n: number): string[];
// @public
export function getIndicesBelow(above: string, n: number): string[];
// @public
export function getIndicesBetween(below: string | undefined, above: string | undefined, n: number): string[];
// @public (undocumented) // @public (undocumented)
export function getPointerInfo(e: PointerEvent | React.PointerEvent): { export function getPointerInfo(e: PointerEvent | React.PointerEvent): {
point: { point: {
@ -1744,11 +1724,6 @@ export class SnapManager {
readonly shapeBounds: BoundsSnaps; readonly shapeBounds: BoundsSnaps;
} }
// @public
export function sortByIndex<T extends {
index: string;
}>(a: T, b: T): -1 | 0 | 1;
// @public (undocumented) // @public (undocumented)
export class Stadium2d extends Ellipse2d { export class Stadium2d extends Ellipse2d {
constructor(config: Omit<Geometry2dOptions, 'isClosed'> & { constructor(config: Omit<Geometry2dOptions, 'isClosed'> & {

View file

@ -10585,8 +10585,9 @@
"text": "): " "text": "): "
}, },
{ {
"kind": "Content", "kind": "Reference",
"text": "string" "text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
}, },
{ {
"kind": "Content", "kind": "Content",
@ -15742,8 +15743,9 @@
"text": ", insertIndex?: " "text": ", insertIndex?: "
}, },
{ {
"kind": "Content", "kind": "Reference",
"text": "string" "text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
}, },
{ {
"kind": "Content", "kind": "Content",
@ -21639,417 +21641,6 @@
], ],
"name": "getIncrementedName" "name": "getIncrementedName"
}, },
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!getIndexAbove:function(1)",
"docComment": "/**\n * Get the index above a given index.\n *\n * @param below - The index below.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndexAbove(below: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/utils/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "below",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"name": "getIndexAbove"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!getIndexBelow:function(1)",
"docComment": "/**\n * Get the index below a given index.\n *\n * @param above - The index above.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndexBelow(above: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/utils/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "above",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"name": "getIndexBelow"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!getIndexBetween:function(1)",
"docComment": "/**\n * Get the index between two indices.\n *\n * @param below - The index below.\n *\n * @param above - The index above.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndexBetween(below: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": ", above?: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/utils/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "below",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "above",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": true
}
],
"name": "getIndexBetween"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!getIndices:function(1)",
"docComment": "/**\n * Get n number of indices, starting at an index.\n *\n * @param n - The number of indices to get.\n *\n * @param start - The index to start at.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndices(n: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ", start?: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "string[]"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/utils/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "n",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "start",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": true
}
],
"name": "getIndices"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!getIndicesAbove:function(1)",
"docComment": "/**\n * Get a number of indices above an index.\n *\n * @param below - The index below.\n *\n * @param n - The number of indices to get.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndicesAbove(below: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": ", n: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "string[]"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/utils/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "below",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "n",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
}
],
"name": "getIndicesAbove"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!getIndicesBelow:function(1)",
"docComment": "/**\n * Get a number of indices below an index.\n *\n * @param above - The index above.\n *\n * @param n - The number of indices to get.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndicesBelow(above: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": ", n: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "string[]"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/utils/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "above",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "n",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
}
],
"name": "getIndicesBelow"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!getIndicesBetween:function(1)",
"docComment": "/**\n * Get a number of indices between two indices.\n *\n * @param below - The index below.\n *\n * @param above - The index above.\n *\n * @param n - The number of indices to get.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndicesBetween(below: "
},
{
"kind": "Content",
"text": "string | undefined"
},
{
"kind": "Content",
"text": ", above: "
},
{
"kind": "Content",
"text": "string | undefined"
},
{
"kind": "Content",
"text": ", n: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "string[]"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/utils/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 7,
"endIndex": 8
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "below",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "above",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
},
{
"parameterName": "n",
"parameterTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"isOptional": false
}
],
"name": "getIndicesBetween"
},
{ {
"kind": "Function", "kind": "Function",
"canonicalReference": "@tldraw/editor!getPointerInfo:function(1)", "canonicalReference": "@tldraw/editor!getPointerInfo:function(1)",
@ -32799,88 +32390,6 @@
], ],
"implementsTokenRanges": [] "implementsTokenRanges": []
}, },
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!sortByIndex:function(1)",
"docComment": "/**\n * Sort by index.\n *\n * @param a - An object with an index property.\n *\n * @param b - An object with an index property.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function sortByIndex<T extends "
},
{
"kind": "Content",
"text": "{\n index: string;\n}"
},
{
"kind": "Content",
"text": ">(a: "
},
{
"kind": "Content",
"text": "T"
},
{
"kind": "Content",
"text": ", b: "
},
{
"kind": "Content",
"text": "T"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "-1 | 0 | 1"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/utils/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 7,
"endIndex": 8
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "a",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
},
{
"parameterName": "b",
"parameterTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"isOptional": false
}
],
"typeParameters": [
{
"typeParameterName": "T",
"constraintTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"defaultTypeTokenRange": {
"startIndex": 0,
"endIndex": 0
}
}
],
"name": "sortByIndex"
},
{ {
"kind": "Class", "kind": "Class",
"canonicalReference": "@tldraw/editor!Stadium2d:class", "canonicalReference": "@tldraw/editor!Stadium2d:class",

View file

@ -356,16 +356,6 @@ export { getSvgPathFromPoints } from './lib/utils/getSvgPathFromPoints'
export { hardResetEditor } from './lib/utils/hardResetEditor' export { hardResetEditor } from './lib/utils/hardResetEditor'
export { normalizeWheel } from './lib/utils/normalizeWheel' export { normalizeWheel } from './lib/utils/normalizeWheel'
export { refreshPage } from './lib/utils/refreshPage' export { refreshPage } from './lib/utils/refreshPage'
export {
getIndexAbove,
getIndexBelow,
getIndexBetween,
getIndices,
getIndicesAbove,
getIndicesBelow,
getIndicesBetween,
sortByIndex,
} from './lib/utils/reordering/reordering'
export { export {
applyRotationToSnapshotShapes, applyRotationToSnapshotShapes,
getRotationSnapshot, getRotationSnapshot,

View file

@ -39,15 +39,22 @@ import {
isShapeId, isShapeId,
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { import {
IndexKey,
JsonObject, JsonObject,
annotateError, annotateError,
assert, assert,
compact, compact,
dedupe, dedupe,
deepCopy, deepCopy,
getIndexAbove,
getIndexBetween,
getIndices,
getIndicesAbove,
getIndicesBetween,
getOwnProperty, getOwnProperty,
hasOwnProperty, hasOwnProperty,
sortById, sortById,
sortByIndex,
structuredClone, structuredClone,
} from '@tldraw/utils' } from '@tldraw/utils'
import { EventEmitter } from 'eventemitter3' import { EventEmitter } from 'eventemitter3'
@ -89,14 +96,6 @@ import { WeakMapCache } from '../utils/WeakMapCache'
import { dataUrlToFile } from '../utils/assets' import { dataUrlToFile } from '../utils/assets'
import { getIncrementedName } from '../utils/getIncrementedName' import { getIncrementedName } from '../utils/getIncrementedName'
import { getReorderingShapesChanges } from '../utils/reorderShapes' import { getReorderingShapesChanges } from '../utils/reorderShapes'
import {
getIndexAbove,
getIndexBetween,
getIndices,
getIndicesAbove,
getIndicesBetween,
sortByIndex,
} from '../utils/reordering/reordering'
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation' import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
import { uniqueId } from '../utils/uniqueId' import { uniqueId } from '../utils/uniqueId'
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex' import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
@ -324,7 +323,7 @@ export class Editor extends EventEmitter<TLEventMap> {
return return
} }
let finalIndex: string let finalIndex: IndexKey
const higherSiblings = this.getSortedChildIdsForParent(highestSibling.parentId) const higherSiblings = this.getSortedChildIdsForParent(highestSibling.parentId)
.map((id) => this.getShape(id)!) .map((id) => this.getShape(id)!)
@ -4775,7 +4774,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* *
* @public * @public
*/ */
reparentShapes(shapes: TLShapeId[] | TLShape[], parentId: TLParentId, insertIndex?: string) { reparentShapes(shapes: TLShapeId[] | TLShape[], parentId: TLParentId, insertIndex?: IndexKey) {
const ids = const ids =
typeof shapes[0] === 'string' ? (shapes as TLShapeId[]) : shapes.map((s) => (s as TLShape).id) typeof shapes[0] === 'string' ? (shapes as TLShapeId[]) : shapes.map((s) => (s as TLShape).id)
if (ids.length === 0) return this if (ids.length === 0) return this
@ -4788,7 +4787,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const parentPageRotation = parentTransform.rotation() const parentPageRotation = parentTransform.rotation()
let indices: string[] = [] let indices: IndexKey[] = []
const sibs = compact(this.getSortedChildIdsForParent(parentId).map((id) => this.getShape(id))) const sibs = compact(this.getSortedChildIdsForParent(parentId).map((id) => this.getShape(id)))
@ -4877,12 +4876,12 @@ export class Editor extends EventEmitter<TLEventMap> {
* *
* @public * @public
*/ */
getHighestIndexForParent(parent: TLParentId | TLPage | TLShape): string { getHighestIndexForParent(parent: TLParentId | TLPage | TLShape): IndexKey {
const parentId = typeof parent === 'string' ? parent : parent.id const parentId = typeof parent === 'string' ? parent : parent.id
const children = this._parentIdsToChildIds.get()[parentId] const children = this._parentIdsToChildIds.get()[parentId]
if (!children || children.length === 0) { if (!children || children.length === 0) {
return 'a1' return 'a1' as IndexKey
} }
const shape = this.getShape(children[children.length - 1])! const shape = this.getShape(children[children.length - 1])!
return getIndexAbove(shape.index) return getIndexAbove(shape.index)
@ -6584,7 +6583,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// Get the highest index among the parents of each of the // Get the highest index among the parents of each of the
// the shapes being created; we'll increment from there. // the shapes being created; we'll increment from there.
const parentIndices = new Map<string, string>() const parentIndices = new Map<TLParentId, IndexKey>()
const shapeRecordsToCreate: TLShape[] = [] const shapeRecordsToCreate: TLShape[] = []

View file

@ -1,8 +1,7 @@
import { computed, isUninitialized, RESET_VALUE } from '@tldraw/state' import { computed, isUninitialized, RESET_VALUE } from '@tldraw/state'
import { RecordsDiff } from '@tldraw/store' import { RecordsDiff } from '@tldraw/store'
import { isShape, TLParentId, TLRecord, TLShape, TLShapeId, TLStore } from '@tldraw/tlschema' import { isShape, TLParentId, TLRecord, TLShape, TLShapeId, TLStore } from '@tldraw/tlschema'
import { compact } from '@tldraw/utils' import { compact, sortByIndex } from '@tldraw/utils'
import { sortByIndex } from '../../utils/reordering/reordering'
type Parents2Children = Record<TLParentId, TLShapeId[]> type Parents2Children = Record<TLParentId, TLShapeId[]>

View file

@ -1,7 +1,6 @@
import { TLParentId, TLShape, TLShapeId, TLShapePartial } from '@tldraw/tlschema' import { TLParentId, TLShape, TLShapeId, TLShapePartial } from '@tldraw/tlschema'
import { compact } from '@tldraw/utils' import { IndexKey, compact, getIndicesBetween, sortByIndex } from '@tldraw/utils'
import { Editor } from '../editor/Editor' import { Editor } from '../editor/Editor'
import { getIndicesBetween, sortByIndex } from './reordering/reordering'
export function getReorderingShapesChanges( export function getReorderingShapesChanges(
editor: Editor, editor: Editor,
@ -63,8 +62,8 @@ function reorderToBack(moving: Set<TLShape>, children: TLShape[], changes: TLSha
// If all of the children are moving, there's nothing to do // If all of the children are moving, there's nothing to do
if (moving.size === len) return if (moving.size === len) return
let below: string | undefined let below: IndexKey | undefined
let above: string | undefined let above: IndexKey | undefined
// Starting at the bottom of this parent's children... // Starting at the bottom of this parent's children...
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
@ -112,8 +111,8 @@ function reorderToFront(moving: Set<TLShape>, children: TLShape[], changes: TLSh
// If all of the children are moving, there's nothing to do // If all of the children are moving, there's nothing to do
if (moving.size === len) return if (moving.size === len) return
let below: string | undefined let below: IndexKey | undefined
let above: string | undefined let above: IndexKey | undefined
// Starting at the top of this parent's children... // Starting at the top of this parent's children...
for (let i = len - 1; i > -1; i--) { for (let i = len - 1; i > -1; i--) {

View file

@ -1,5 +1,5 @@
import { PageRecordType } from '@tldraw/tlschema' import { PageRecordType } from '@tldraw/tlschema'
import { promiseWithResolve } from '@tldraw/utils' import { IndexKey, promiseWithResolve } from '@tldraw/utils'
import { createTLStore } from '../../config/createTLStore' import { createTLStore } from '../../config/createTLStore'
import { TLLocalSyncClient } from './TLLocalSyncClient' import { TLLocalSyncClient } from './TLLocalSyncClient'
import * as idb from './indexedDb' import * as idb from './indexedDb'
@ -111,12 +111,12 @@ test('when a client receives an announce with a newer schema version shortly aft
test('the first db write after a client connects is a full db overwrite', async () => { test('the first db write after a client connects is a full db overwrite', async () => {
const { client } = testClient() const { client } = testClient()
await tick() await tick()
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' })]) client.store.put([PageRecordType.create({ name: 'test', index: 'a0' as IndexKey })])
await tick() await tick()
expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1) expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1)
expect(idb.storeChangesInIndexedDb).not.toHaveBeenCalled() expect(idb.storeChangesInIndexedDb).not.toHaveBeenCalled()
client.store.put([PageRecordType.create({ name: 'test2', index: 'a1' })]) client.store.put([PageRecordType.create({ name: 'test2', index: 'a1' as IndexKey })])
await tick() await tick()
expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1) expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1)
expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(1) expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(1)
@ -125,12 +125,12 @@ test('the first db write after a client connects is a full db overwrite', async
test('it clears the diff queue after every write', async () => { test('it clears the diff queue after every write', async () => {
const { client } = testClient() const { client } = testClient()
await tick() await tick()
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' })]) client.store.put([PageRecordType.create({ name: 'test', index: 'a0' as IndexKey })])
await tick() await tick()
// @ts-expect-error // @ts-expect-error
expect(client.diffQueue.length).toBe(0) expect(client.diffQueue.length).toBe(0)
client.store.put([PageRecordType.create({ name: 'test2', index: 'a1' })]) client.store.put([PageRecordType.create({ name: 'test2', index: 'a1' as IndexKey })])
await tick() await tick()
// @ts-expect-error // @ts-expect-error
expect(client.diffQueue.length).toBe(0) expect(client.diffQueue.length).toBe(0)
@ -142,7 +142,7 @@ test('writes that come in during a persist operation will get persisted afterwar
const { client } = testClient() const { client } = testClient()
await tick() await tick()
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' })]) client.store.put([PageRecordType.create({ name: 'test', index: 'a0' as IndexKey })])
await tick() await tick()
// we should have called into idb but not resolved the promise yet // we should have called into idb but not resolved the promise yet
@ -150,7 +150,7 @@ test('writes that come in during a persist operation will get persisted afterwar
expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(0) expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(0)
// if another change comes in, loads of time can pass, but nothing else should get called // if another change comes in, loads of time can pass, but nothing else should get called
client.store.put([PageRecordType.create({ name: 'test', index: 'a2' })]) client.store.put([PageRecordType.create({ name: 'test', index: 'a2' as IndexKey })])
await tick() await tick()
expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1) expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1)
expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(0) expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(0)

View file

@ -20,6 +20,7 @@ import { EmbedDefinition } from '@tldraw/editor';
import { EnumStyleProp } from '@tldraw/editor'; import { EnumStyleProp } from '@tldraw/editor';
import { Geometry2d } from '@tldraw/editor'; import { Geometry2d } from '@tldraw/editor';
import { Group2d } from '@tldraw/editor'; import { Group2d } from '@tldraw/editor';
import { IndexKey } from '@tldraw/editor';
import { JsonObject } from '@tldraw/editor'; import { JsonObject } from '@tldraw/editor';
import { JSX as JSX_2 } from 'react/jsx-runtime'; import { JSX as JSX_2 } from 'react/jsx-runtime';
import { LANGUAGES } from '@tldraw/editor'; import { LANGUAGES } from '@tldraw/editor';
@ -603,7 +604,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
x: number; x: number;
y: number; y: number;
rotation: number; rotation: number;
index: string; index: IndexKey;
parentId: TLParentId; parentId: TLParentId;
isLocked: boolean; isLocked: boolean;
opacity: number; opacity: number;
@ -633,7 +634,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
x: number; x: number;
y: number; y: number;
rotation: number; rotation: number;
index: string; index: IndexKey;
parentId: TLParentId; parentId: TLParentId;
isLocked: boolean; isLocked: boolean;
opacity: number; opacity: number;
@ -650,7 +651,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
x: number; x: number;
y: number; y: number;
rotation: number; rotation: number;
index: string; index: IndexKey;
parentId: TLParentId; parentId: TLParentId;
isLocked: boolean; isLocked: boolean;
opacity: number; opacity: number;
@ -665,7 +666,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
x: number; x: number;
y: number; y: number;
rotation: number; rotation: number;
index: string; index: IndexKey;
parentId: TLParentId; parentId: TLParentId;
isLocked: boolean; isLocked: boolean;
opacity: number; opacity: number;
@ -999,7 +1000,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
x: number; x: number;
y: number; y: number;
rotation: number; rotation: number;
index: string; index: IndexKey;
parentId: TLParentId; parentId: TLParentId;
isLocked: boolean; isLocked: boolean;
opacity: number; opacity: number;
@ -1023,7 +1024,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
x: number; x: number;
y: number; y: number;
rotation: number; rotation: number;
index: string; index: IndexKey;
parentId: TLParentId; parentId: TLParentId;
isLocked: boolean; isLocked: boolean;
opacity: number; opacity: number;
@ -1166,7 +1167,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
y: number; y: number;
type: "text"; type: "text";
rotation: number; rotation: number;
index: string; index: IndexKey;
parentId: TLParentId; parentId: TLParentId;
isLocked: boolean; isLocked: boolean;
opacity: number; opacity: number;
@ -1200,7 +1201,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
}; };
type: "text"; type: "text";
rotation: number; rotation: number;
index: string; index: IndexKey;
parentId: TLParentId; parentId: TLParentId;
isLocked: boolean; isLocked: boolean;
opacity: number; opacity: number;

View file

@ -7342,7 +7342,16 @@
}, },
{ {
"kind": "Content", "kind": "Content",
"text": ") => {\n props: {\n growY: number;\n geo: \"arrow-down\" | \"arrow-left\" | \"arrow-right\" | \"arrow-up\" | \"check-box\" | \"cloud\" | \"diamond\" | \"ellipse\" | \"hexagon\" | \"octagon\" | \"oval\" | \"pentagon\" | \"rectangle\" | \"rhombus-2\" | \"rhombus\" | \"star\" | \"trapezoid\" | \"triangle\" | \"x-box\";\n labelColor: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n color: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n fill: \"none\" | \"pattern\" | \"semi\" | \"solid\";\n dash: \"dashed\" | \"dotted\" | \"draw\" | \"solid\";\n size: \"l\" | \"m\" | \"s\" | \"xl\";\n font: \"draw\" | \"mono\" | \"sans\" | \"serif\";\n align: \"end-legacy\" | \"end\" | \"middle-legacy\" | \"middle\" | \"start-legacy\" | \"start\";\n verticalAlign: \"end\" | \"middle\" | \"start\";\n url: string;\n w: number;\n h: number;\n text: string;\n };\n type: \"geo\";\n x: number;\n y: number;\n rotation: number;\n index: string;\n parentId: import(\"@tldraw/editor\")." "text": ") => {\n props: {\n growY: number;\n geo: \"arrow-down\" | \"arrow-left\" | \"arrow-right\" | \"arrow-up\" | \"check-box\" | \"cloud\" | \"diamond\" | \"ellipse\" | \"hexagon\" | \"octagon\" | \"oval\" | \"pentagon\" | \"rectangle\" | \"rhombus-2\" | \"rhombus\" | \"star\" | \"trapezoid\" | \"triangle\" | \"x-box\";\n labelColor: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n color: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n fill: \"none\" | \"pattern\" | \"semi\" | \"solid\";\n dash: \"dashed\" | \"dotted\" | \"draw\" | \"solid\";\n size: \"l\" | \"m\" | \"s\" | \"xl\";\n font: \"draw\" | \"mono\" | \"sans\" | \"serif\";\n align: \"end-legacy\" | \"end\" | \"middle-legacy\" | \"middle\" | \"start-legacy\" | \"start\";\n verticalAlign: \"end\" | \"middle\" | \"start\";\n url: string;\n w: number;\n h: number;\n text: string;\n };\n type: \"geo\";\n x: number;\n y: number;\n rotation: number;\n index: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";\n parentId: import(\"@tldraw/editor\")."
}, },
{ {
"kind": "Reference", "kind": "Reference",
@ -7382,7 +7391,7 @@
"name": "onBeforeCreate", "name": "onBeforeCreate",
"propertyTypeTokenRange": { "propertyTypeTokenRange": {
"startIndex": 1, "startIndex": 1,
"endIndex": 10 "endIndex": 12
}, },
"isStatic": false, "isStatic": false,
"isProtected": false, "isProtected": false,
@ -7417,7 +7426,16 @@
}, },
{ {
"kind": "Content", "kind": "Content",
"text": ") => {\n props: {\n growY: number;\n geo: \"arrow-down\" | \"arrow-left\" | \"arrow-right\" | \"arrow-up\" | \"check-box\" | \"cloud\" | \"diamond\" | \"ellipse\" | \"hexagon\" | \"octagon\" | \"oval\" | \"pentagon\" | \"rectangle\" | \"rhombus-2\" | \"rhombus\" | \"star\" | \"trapezoid\" | \"triangle\" | \"x-box\";\n labelColor: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n color: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n fill: \"none\" | \"pattern\" | \"semi\" | \"solid\";\n dash: \"dashed\" | \"dotted\" | \"draw\" | \"solid\";\n size: \"l\" | \"m\" | \"s\" | \"xl\";\n font: \"draw\" | \"mono\" | \"sans\" | \"serif\";\n align: \"end-legacy\" | \"end\" | \"middle-legacy\" | \"middle\" | \"start-legacy\" | \"start\";\n verticalAlign: \"end\" | \"middle\" | \"start\";\n url: string;\n w: number;\n h: number;\n text: string;\n };\n type: \"geo\";\n x: number;\n y: number;\n rotation: number;\n index: string;\n parentId: import(\"@tldraw/editor\")." "text": ") => {\n props: {\n growY: number;\n geo: \"arrow-down\" | \"arrow-left\" | \"arrow-right\" | \"arrow-up\" | \"check-box\" | \"cloud\" | \"diamond\" | \"ellipse\" | \"hexagon\" | \"octagon\" | \"oval\" | \"pentagon\" | \"rectangle\" | \"rhombus-2\" | \"rhombus\" | \"star\" | \"trapezoid\" | \"triangle\" | \"x-box\";\n labelColor: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n color: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n fill: \"none\" | \"pattern\" | \"semi\" | \"solid\";\n dash: \"dashed\" | \"dotted\" | \"draw\" | \"solid\";\n size: \"l\" | \"m\" | \"s\" | \"xl\";\n font: \"draw\" | \"mono\" | \"sans\" | \"serif\";\n align: \"end-legacy\" | \"end\" | \"middle-legacy\" | \"middle\" | \"start-legacy\" | \"start\";\n verticalAlign: \"end\" | \"middle\" | \"start\";\n url: string;\n w: number;\n h: number;\n text: string;\n };\n type: \"geo\";\n x: number;\n y: number;\n rotation: number;\n index: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";\n parentId: import(\"@tldraw/editor\")."
}, },
{ {
"kind": "Reference", "kind": "Reference",
@ -7457,7 +7475,7 @@
"name": "onBeforeUpdate", "name": "onBeforeUpdate",
"propertyTypeTokenRange": { "propertyTypeTokenRange": {
"startIndex": 1, "startIndex": 1,
"endIndex": 12 "endIndex": 14
}, },
"isStatic": false, "isStatic": false,
"isProtected": false, "isProtected": false,
@ -7483,7 +7501,16 @@
}, },
{ {
"kind": "Content", "kind": "Content",
"text": ") => {\n props: {\n geo: \"check-box\";\n };\n type: \"geo\";\n x: number;\n y: number;\n rotation: number;\n index: string;\n parentId: import(\"@tldraw/editor\")." "text": ") => {\n props: {\n geo: \"check-box\";\n };\n type: \"geo\";\n x: number;\n y: number;\n rotation: number;\n index: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";\n parentId: import(\"@tldraw/editor\")."
}, },
{ {
"kind": "Reference", "kind": "Reference",
@ -7510,7 +7537,16 @@
}, },
{ {
"kind": "Content", "kind": "Content",
"text": ";\n typeName: \"shape\";\n } | {\n props: {\n geo: \"rectangle\";\n };\n type: \"geo\";\n x: number;\n y: number;\n rotation: number;\n index: string;\n parentId: import(\"@tldraw/editor\")." "text": ";\n typeName: \"shape\";\n } | {\n props: {\n geo: \"rectangle\";\n };\n type: \"geo\";\n x: number;\n y: number;\n rotation: number;\n index: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";\n parentId: import(\"@tldraw/editor\")."
}, },
{ {
"kind": "Reference", "kind": "Reference",
@ -7550,7 +7586,7 @@
"name": "onDoubleClick", "name": "onDoubleClick",
"propertyTypeTokenRange": { "propertyTypeTokenRange": {
"startIndex": 1, "startIndex": 1,
"endIndex": 16 "endIndex": 20
}, },
"isStatic": false, "isStatic": false,
"isProtected": false, "isProtected": false,
@ -11997,7 +12033,16 @@
}, },
{ {
"kind": "Content", "kind": "Content",
"text": ") => {\n props: {\n growY: number;\n color: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n size: \"l\" | \"m\" | \"s\" | \"xl\";\n font: \"draw\" | \"mono\" | \"sans\" | \"serif\";\n align: \"end-legacy\" | \"end\" | \"middle-legacy\" | \"middle\" | \"start-legacy\" | \"start\";\n verticalAlign: \"end\" | \"middle\" | \"start\";\n url: string;\n text: string;\n };\n type: \"note\";\n x: number;\n y: number;\n rotation: number;\n index: string;\n parentId: import(\"@tldraw/editor\")." "text": ") => {\n props: {\n growY: number;\n color: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n size: \"l\" | \"m\" | \"s\" | \"xl\";\n font: \"draw\" | \"mono\" | \"sans\" | \"serif\";\n align: \"end-legacy\" | \"end\" | \"middle-legacy\" | \"middle\" | \"start-legacy\" | \"start\";\n verticalAlign: \"end\" | \"middle\" | \"start\";\n url: string;\n text: string;\n };\n type: \"note\";\n x: number;\n y: number;\n rotation: number;\n index: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";\n parentId: import(\"@tldraw/editor\")."
}, },
{ {
"kind": "Reference", "kind": "Reference",
@ -12037,7 +12082,7 @@
"name": "onBeforeCreate", "name": "onBeforeCreate",
"propertyTypeTokenRange": { "propertyTypeTokenRange": {
"startIndex": 1, "startIndex": 1,
"endIndex": 10 "endIndex": 12
}, },
"isStatic": false, "isStatic": false,
"isProtected": false, "isProtected": false,
@ -12072,7 +12117,16 @@
}, },
{ {
"kind": "Content", "kind": "Content",
"text": ") => {\n props: {\n growY: number;\n color: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n size: \"l\" | \"m\" | \"s\" | \"xl\";\n font: \"draw\" | \"mono\" | \"sans\" | \"serif\";\n align: \"end-legacy\" | \"end\" | \"middle-legacy\" | \"middle\" | \"start-legacy\" | \"start\";\n verticalAlign: \"end\" | \"middle\" | \"start\";\n url: string;\n text: string;\n };\n type: \"note\";\n x: number;\n y: number;\n rotation: number;\n index: string;\n parentId: import(\"@tldraw/editor\")." "text": ") => {\n props: {\n growY: number;\n color: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n size: \"l\" | \"m\" | \"s\" | \"xl\";\n font: \"draw\" | \"mono\" | \"sans\" | \"serif\";\n align: \"end-legacy\" | \"end\" | \"middle-legacy\" | \"middle\" | \"start-legacy\" | \"start\";\n verticalAlign: \"end\" | \"middle\" | \"start\";\n url: string;\n text: string;\n };\n type: \"note\";\n x: number;\n y: number;\n rotation: number;\n index: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";\n parentId: import(\"@tldraw/editor\")."
}, },
{ {
"kind": "Reference", "kind": "Reference",
@ -12112,7 +12166,7 @@
"name": "onBeforeUpdate", "name": "onBeforeUpdate",
"propertyTypeTokenRange": { "propertyTypeTokenRange": {
"startIndex": 1, "startIndex": 1,
"endIndex": 12 "endIndex": 14
}, },
"isStatic": false, "isStatic": false,
"isProtected": false, "isProtected": false,
@ -13660,7 +13714,16 @@
}, },
{ {
"kind": "Content", "kind": "Content",
"text": ") => {\n x: number;\n y: number;\n type: \"text\";\n rotation: number;\n index: string;\n parentId: import(\"@tldraw/editor\")." "text": ") => {\n x: number;\n y: number;\n type: \"text\";\n rotation: number;\n index: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";\n parentId: import(\"@tldraw/editor\")."
}, },
{ {
"kind": "Reference", "kind": "Reference",
@ -13700,7 +13763,7 @@
"name": "onBeforeCreate", "name": "onBeforeCreate",
"propertyTypeTokenRange": { "propertyTypeTokenRange": {
"startIndex": 1, "startIndex": 1,
"endIndex": 10 "endIndex": 12
}, },
"isStatic": false, "isStatic": false,
"isProtected": false, "isProtected": false,
@ -13735,7 +13798,16 @@
}, },
{ {
"kind": "Content", "kind": "Content",
"text": ") => {\n x: number;\n y: number;\n props: {\n w: number;\n color: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n size: \"l\" | \"m\" | \"s\" | \"xl\";\n font: \"draw\" | \"mono\" | \"sans\" | \"serif\";\n align: \"end-legacy\" | \"end\" | \"middle-legacy\" | \"middle\" | \"start-legacy\" | \"start\";\n text: string;\n scale: number;\n autoSize: boolean;\n };\n type: \"text\";\n rotation: number;\n index: string;\n parentId: import(\"@tldraw/editor\")." "text": ") => {\n x: number;\n y: number;\n props: {\n w: number;\n color: \"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\";\n size: \"l\" | \"m\" | \"s\" | \"xl\";\n font: \"draw\" | \"mono\" | \"sans\" | \"serif\";\n align: \"end-legacy\" | \"end\" | \"middle-legacy\" | \"middle\" | \"start-legacy\" | \"start\";\n text: string;\n scale: number;\n autoSize: boolean;\n };\n type: \"text\";\n rotation: number;\n index: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";\n parentId: import(\"@tldraw/editor\")."
}, },
{ {
"kind": "Reference", "kind": "Reference",
@ -13775,7 +13847,7 @@
"name": "onBeforeUpdate", "name": "onBeforeUpdate",
"propertyTypeTokenRange": { "propertyTypeTokenRange": {
"startIndex": 1, "startIndex": 1,
"endIndex": 12 "endIndex": 14
}, },
"isStatic": false, "isStatic": false,
"isProtected": false, "isProtected": false,

View file

@ -1,4 +1,4 @@
import { TLArrowShape, Vec, createShapeId } from '@tldraw/editor' import { IndexKey, TLArrowShape, Vec, createShapeId } from '@tldraw/editor'
import { TestEditor } from '../../../test/TestEditor' import { TestEditor } from '../../../test/TestEditor'
let editor: TestEditor let editor: TestEditor
@ -406,14 +406,14 @@ describe('reparenting issue', () => {
{ id: ids.box3, type: 'geo', x: 350, y: 350, props: { w: 90, h: 90 } }, // overlapping box2 { id: ids.box3, type: 'geo', x: 350, y: 350, props: { w: 90, h: 90 } }, // overlapping box2
]) ])
editor.expectShapeToMatch({ id: ids.box1, index: 'a2' }) editor.expectShapeToMatch({ id: ids.box1, index: 'a2' as IndexKey })
editor.expectShapeToMatch({ id: ids.box2, index: 'a3' }) editor.expectShapeToMatch({ id: ids.box2, index: 'a3' as IndexKey })
editor.expectShapeToMatch({ id: ids.box3, index: 'a4' }) editor.expectShapeToMatch({ id: ids.box3, index: 'a4' as IndexKey })
editor.select(arrowId) editor.select(arrowId)
editor.pointerDown(100, 100, { editor.pointerDown(100, 100, {
target: 'handle', target: 'handle',
handle: { id: 'end', type: 'vertex', index: 'a0', x: 100, y: 100 }, handle: { id: 'end', type: 'vertex', index: 'a0' as IndexKey, x: 100, y: 100 },
shape: editor.getShape(arrowId)!, shape: editor.getShape(arrowId)!,
}) })
editor.expectToBeIn('select.pointing_handle') editor.expectToBeIn('select.pointing_handle')
@ -422,7 +422,7 @@ describe('reparenting issue', () => {
editor.expectToBeIn('select.dragging_handle') editor.expectToBeIn('select.dragging_handle')
editor.expectShapeToMatch({ editor.expectShapeToMatch({
id: arrowId, id: arrowId,
index: 'a3V', index: 'a3V' as IndexKey,
props: { end: { boundShapeId: ids.box2 } }, props: { end: { boundShapeId: ids.box2 } },
}) // between box 2 (a3) and 3 (a4) }) // between box 2 (a3) and 3 (a4)
@ -433,15 +433,15 @@ describe('reparenting issue', () => {
editor.pointerMove(350, 350) // over box 3 and box 2, but box 3 is smaller editor.pointerMove(350, 350) // over box 3 and box 2, but box 3 is smaller
editor.expectShapeToMatch({ editor.expectShapeToMatch({
id: arrowId, id: arrowId,
index: 'a5', index: 'a5' as IndexKey,
props: { end: { boundShapeId: ids.box3 } }, props: { end: { boundShapeId: ids.box3 } },
}) // above box 3 (a4) }) // above box 3 (a4)
editor.pointerMove(150, 150) // over box 1 editor.pointerMove(150, 150) // over box 1
editor.expectShapeToMatch({ id: arrowId, index: 'a2V' }) // between box 1 (a2) and box 3 (a3) editor.expectShapeToMatch({ id: arrowId, index: 'a2V' as IndexKey }) // between box 1 (a2) and box 3 (a3)
editor.pointerMove(-100, -100) // over the page editor.pointerMove(-100, -100) // over the page
editor.expectShapeToMatch({ id: arrowId, index: 'a2V' }) // no change needed, keep whatever we had before editor.expectShapeToMatch({ id: arrowId, index: 'a2V' as IndexKey }) // no change needed, keep whatever we had before
// todo: should the arrow go back to where it was before? // todo: should the arrow go back to where it was before?
}) })
@ -481,7 +481,7 @@ describe('reparenting issue', () => {
.select(arrow1Id) .select(arrow1Id)
.pointerDown(100, 100, { .pointerDown(100, 100, {
target: 'handle', target: 'handle',
handle: { id: 'end', type: 'vertex', index: 'a0', x: 100, y: 100 }, handle: { id: 'end', type: 'vertex', index: 'a0' as IndexKey, x: 100, y: 100 },
shape: editor.getShape(arrow1Id)!, shape: editor.getShape(arrow1Id)!,
}) })
.pointerMove(120, 120) .pointerMove(120, 120)
@ -490,7 +490,7 @@ describe('reparenting issue', () => {
.select(arrow2Id) .select(arrow2Id)
.pointerDown(100, 100, { .pointerDown(100, 100, {
target: 'handle', target: 'handle',
handle: { id: 'end', type: 'vertex', index: 'a0', x: 100, y: 100 }, handle: { id: 'end', type: 'vertex', index: 'a0' as IndexKey, x: 100, y: 100 },
shape: editor.getShape(arrow2Id)!, shape: editor.getShape(arrow2Id)!,
}) })
.pointerMove(150, 150) .pointerMove(150, 150)

View file

@ -1,4 +1,4 @@
import { TLGeoShape, TLLineShape, createShapeId, deepCopy } from '@tldraw/editor' import { IndexKey, TLGeoShape, TLLineShape, createShapeId, deepCopy } from '@tldraw/editor'
import { TestEditor } from '../../../test/TestEditor' import { TestEditor } from '../../../test/TestEditor'
jest.mock('nanoid', () => { jest.mock('nanoid', () => {
@ -85,7 +85,7 @@ it('create new handle', () => {
handle: { handle: {
id: 'mid-0', id: 'mid-0',
type: 'create', type: 'create',
index: 'a1V', index: 'a1V' as IndexKey,
x: 50, x: 50,
y: 50, y: 50,
}, },

View file

@ -1,6 +1,7 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { import {
CubicSpline2d, CubicSpline2d,
IndexKey,
Polyline2d, Polyline2d,
SVGContainer, SVGContainer,
ShapeUtil, ShapeUtil,
@ -55,7 +56,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
type: 'vertex', type: 'vertex',
canBind: false, canBind: false,
canSnap: true, canSnap: true,
index: 'a1', index: 'a1' as IndexKey,
x: 0, x: 0,
y: 0, y: 0,
}, },
@ -64,7 +65,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
type: 'vertex', type: 'vertex',
canBind: false, canBind: false,
canSnap: true, canSnap: true,
index: 'a2', index: 'a2' as IndexKey,
x: 0.1, x: 0.1,
y: 0.1, y: 0.1,
}, },

View file

@ -1,4 +1,5 @@
import { import {
IndexKey,
Mat, Mat,
StateNode, StateNode,
TLEventHandlers, TLEventHandlers,
@ -50,7 +51,7 @@ export class Pointing extends StateNode {
new Vec(this.shape.x, this.shape.y) new Vec(this.shape.x, this.shape.y)
) )
let nextEndHandleIndex: string, nextEndHandleId: string, nextEndHandle: TLHandle let nextEndHandleIndex: IndexKey, nextEndHandleId: string, nextEndHandle: TLHandle
const nextPoint = Vec.Sub(currentPagePoint, shapePagePoint) const nextPoint = Vec.Sub(currentPagePoint, shapePagePoint)

View file

@ -1,7 +1,14 @@
import { Editor, getIndexAbove, getIndexBelow, getIndexBetween, TLPageId } from '@tldraw/editor' import {
Editor,
getIndexAbove,
getIndexBelow,
getIndexBetween,
IndexKey,
TLPageId,
} from '@tldraw/editor'
export const onMovePage = (editor: Editor, id: TLPageId, from: number, to: number) => { export const onMovePage = (editor: Editor, id: TLPageId, from: number, to: number) => {
let index: string let index: IndexKey
const pages = editor.getPages() const pages = editor.getPages()

View file

@ -15,6 +15,7 @@ import {
TLShapeId, TLShapeId,
Vec, Vec,
VecLike, VecLike,
ZERO_INDEX_KEY,
compact, compact,
createShapeId, createShapeId,
getIndexAbove, getIndexAbove,
@ -63,7 +64,7 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
} }
}) })
let index = 'a1' let index = ZERO_INDEX_KEY
for (const element of elements) { for (const element of elements) {
if (skipIds.has(element.id)) { if (skipIds.has(element.id)) {

View file

@ -1,4 +1,4 @@
import { createShapeId } from '@tldraw/editor' import { IndexKey, createShapeId } from '@tldraw/editor'
import { TestEditor } from './TestEditor' import { TestEditor } from './TestEditor'
let editor: TestEditor let editor: TestEditor
@ -114,7 +114,7 @@ describe('PointingHandle', () => {
editor.pointerDown(150, 150, { editor.pointerDown(150, 150, {
target: 'handle', target: 'handle',
shape, shape,
handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 }, handle: { id: 'start', type: 'vertex', index: 'a1' as IndexKey, x: 0, y: 0 },
}) })
editor.expectToBeIn('select.pointing_handle') editor.expectToBeIn('select.pointing_handle')
@ -127,7 +127,7 @@ describe('PointingHandle', () => {
editor.pointerDown(150, 150, { editor.pointerDown(150, 150, {
target: 'handle', target: 'handle',
shape, shape,
handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 }, handle: { id: 'start', type: 'vertex', index: 'a1' as IndexKey, x: 0, y: 0 },
}) })
editor.expectToBeIn('select.pointing_handle') editor.expectToBeIn('select.pointing_handle')
editor.cancel() editor.cancel()
@ -142,7 +142,7 @@ describe('DraggingHandle', () => {
editor.pointerDown(150, 150, { editor.pointerDown(150, 150, {
target: 'handle', target: 'handle',
shape, shape,
handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 }, handle: { id: 'start', type: 'vertex', index: 'a1' as IndexKey, x: 0, y: 0 },
}) })
editor.pointerMove(100, 100) editor.pointerMove(100, 100)
editor.expectToBeIn('select.dragging_handle') editor.expectToBeIn('select.dragging_handle')
@ -158,7 +158,7 @@ describe('DraggingHandle', () => {
editor.pointerDown(150, 150, { editor.pointerDown(150, 150, {
target: 'handle', target: 'handle',
shape, shape,
handle: { id: 'start', type: 'vertex', index: 'a1', x: 0, y: 0 }, handle: { id: 'start', type: 'vertex', index: 'a1' as IndexKey, x: 0, y: 0 },
}) })
editor.pointerMove(100, 100) editor.pointerMove(100, 100)
editor.expectToBeIn('select.dragging_handle') editor.expectToBeIn('select.dragging_handle')

View file

@ -1,4 +1,4 @@
import { PageRecordType, TLShape, createShapeId } from '@tldraw/editor' import { IndexKey, PageRecordType, TLShape, createShapeId } from '@tldraw/editor'
import { TestEditor } from '../TestEditor' import { TestEditor } from '../TestEditor'
let editor: TestEditor let editor: TestEditor
@ -122,7 +122,7 @@ describe('Editor.moveShapesToPage', () => {
editor.expectShapeToMatch({ editor.expectShapeToMatch({
id: ids.box1, id: ids.box1,
parentId: page2Id, parentId: page2Id,
index: 'a1', index: 'a1' as IndexKey,
}) })
const page3Id = PageRecordType.createId('newPage3') const page3Id = PageRecordType.createId('newPage3')
@ -134,7 +134,7 @@ describe('Editor.moveShapesToPage', () => {
editor.expectShapeToMatch({ editor.expectShapeToMatch({
id: ids.box2, id: ids.box2,
parentId: page3Id, parentId: page3Id,
index: 'a1', index: 'a1' as IndexKey,
}) })
editor.setCurrentPage(page2Id) editor.setCurrentPage(page2Id)
@ -147,12 +147,12 @@ describe('Editor.moveShapesToPage', () => {
{ {
id: ids.box2, id: ids.box2,
parentId: page3Id, parentId: page3Id,
index: 'a1', index: 'a1' as IndexKey,
}, },
{ {
id: ids.box1, id: ids.box1,
parentId: page3Id, parentId: page3Id,
index: 'a2', // should be a2 now index: 'a2' as IndexKey, // should be a2 now
} }
) )
}) })

View file

@ -1,4 +1,4 @@
import { createShapeId } from '@tldraw/editor' import { IndexKey, createShapeId } from '@tldraw/editor'
import { TestEditor, createDefaultShapes, defaultShapesIds } from '../TestEditor' import { TestEditor, createDefaultShapes, defaultShapesIds } from '../TestEditor'
let editor: TestEditor let editor: TestEditor
@ -105,7 +105,7 @@ it('adds children at a given index', () => {
expect(editor.getShape(ids.ellipse1)!.index).toBe('a1') expect(editor.getShape(ids.ellipse1)!.index).toBe('a1')
// Handles collisions (trying to move box3 to a0, but box2 is there already) // Handles collisions (trying to move box3 to a0, but box2 is there already)
editor.reparentShapes([ids.box3], ids.box1, 'a1') editor.reparentShapes([ids.box3], ids.box1, 'a1' as IndexKey)
// Page // Page
// - box1 a1 // - box1 a1
@ -124,7 +124,7 @@ it('adds children at a given index', () => {
// Handles collisions (trying to move box5 to a0, but box2 is there already) // Handles collisions (trying to move box5 to a0, but box2 is there already)
// should end up between box 2 and box 3 (a0 and a1) // should end up between box 2 and box 3 (a0 and a1)
editor.reparentShapes([ids.box5], ids.box1, 'a1') editor.reparentShapes([ids.box5], ids.box1, 'a1' as IndexKey)
// Page // Page
// - box1 a1 // - box1 a1
@ -143,7 +143,7 @@ it('adds children at a given index', () => {
// Handles collisions (trying to move boxes 2, 3, and 5 to a0, but box1 is there already) // Handles collisions (trying to move boxes 2, 3, and 5 to a0, but box1 is there already)
// Should order them between box1 and box4 // Should order them between box1 and box4
editor.reparentShapes([ids.box2, ids.box3, ids.box5], editor.getCurrentPageId(), 'a1') editor.reparentShapes([ids.box2, ids.box3, ids.box5], editor.getCurrentPageId(), 'a1' as IndexKey)
// Page // Page
// - box1 a1 // - box1 a1

View file

@ -1,4 +1,4 @@
import { PageRecordType, TLPageId, createShapeId } from '@tldraw/editor' import { IndexKey, PageRecordType, TLPageId, createShapeId } from '@tldraw/editor'
import { TestEditor } from '../TestEditor' import { TestEditor } from '../TestEditor'
let editor: TestEditor let editor: TestEditor
@ -39,7 +39,7 @@ describe('setCurrentPage', () => {
}) })
it("adding a page to the store by any means adds tab state for the page if it doesn't already exist", () => { it("adding a page to the store by any means adds tab state for the page if it doesn't already exist", () => {
const page = PageRecordType.create({ name: 'test', index: 'a4' }) const page = PageRecordType.create({ name: 'test', index: 'a4' as IndexKey })
expect(editor.getPageStates().find((p) => p.pageId === page.id)).toBeUndefined() expect(editor.getPageStates().find((p) => p.pageId === page.id)).toBeUndefined()
editor.store.put([page]) editor.store.put([page])
expect(editor.getPageStates().find((p) => p.pageId === page.id)).not.toBeUndefined() expect(editor.getPageStates().find((p) => p.pageId === page.id)).not.toBeUndefined()
@ -47,7 +47,7 @@ describe('setCurrentPage', () => {
it('squashes', () => { it('squashes', () => {
const page2Id = PageRecordType.createId('page2') const page2Id = PageRecordType.createId('page2')
editor.createPage({ name: 'New Page 2', index: page2Id }) editor.createPage({ name: 'New Page 2', id: page2Id })
editor.history.clear() editor.history.clear()
editor.setCurrentPage(editor.getPages()[1].id) editor.setCurrentPage(editor.getPages()[1].id)

View file

@ -2,6 +2,7 @@ import {
TLDefaultShape, TLDefaultShape,
TLShapeId, TLShapeId,
TLShapePartial, TLShapePartial,
ZERO_INDEX_KEY,
assert, assert,
assertExists, assertExists,
createShapeId, createShapeId,
@ -55,7 +56,7 @@ export function shapesFromJsx(shapes: React.JSX.Element | Array<React.JSX.Elemen
children: React.JSX.Element | Array<React.JSX.Element>, children: React.JSX.Element | Array<React.JSX.Element>,
parentId?: TLShapeId parentId?: TLShapeId
) { ) {
let nextIndex = 'a0' let nextIndex = ZERO_INDEX_KEY
for (const el of Array.isArray(children) ? children : [children]) { for (const el of Array.isArray(children) ? children : [children]) {
const shapeType = (el.type as any)[shapeTypeSymbol] as string const shapeType = (el.type as any)[shapeTypeSymbol] as string

View file

@ -6,6 +6,7 @@
import { BaseRecord } from '@tldraw/store'; import { BaseRecord } from '@tldraw/store';
import { Expand } from '@tldraw/utils'; import { Expand } from '@tldraw/utils';
import { IndexKey } from '@tldraw/utils';
import { JsonObject } from '@tldraw/utils'; import { JsonObject } from '@tldraw/utils';
import { Migrations } from '@tldraw/store'; import { Migrations } from '@tldraw/store';
import { RecordId } from '@tldraw/store'; import { RecordId } from '@tldraw/store';
@ -820,7 +821,7 @@ export interface TLBaseAsset<Type extends string, Props> extends BaseRecord<'ass
// @public (undocumented) // @public (undocumented)
export interface TLBaseShape<Type extends string, Props extends object> extends BaseRecord<'shape', TLShapeId> { export interface TLBaseShape<Type extends string, Props extends object> extends BaseRecord<'shape', TLShapeId> {
// (undocumented) // (undocumented)
index: string; index: IndexKey;
// (undocumented) // (undocumented)
isLocked: boolean; isLocked: boolean;
// (undocumented) // (undocumented)
@ -968,7 +969,7 @@ export interface TLHandle {
canSnap?: boolean; canSnap?: boolean;
id: string; id: string;
// (undocumented) // (undocumented)
index: string; index: IndexKey;
// (undocumented) // (undocumented)
type: TLHandleType; type: TLHandleType;
// (undocumented) // (undocumented)
@ -1152,7 +1153,7 @@ export type TLOpacityType = number;
// @public // @public
export interface TLPage extends BaseRecord<'page', TLPageId> { export interface TLPage extends BaseRecord<'page', TLPageId> {
// (undocumented) // (undocumented)
index: string; index: IndexKey;
// (undocumented) // (undocumented)
meta: JsonObject; meta: JsonObject;
// (undocumented) // (undocumented)

View file

@ -4322,8 +4322,9 @@
"text": "index: " "text": "index: "
}, },
{ {
"kind": "Content", "kind": "Reference",
"text": "string" "text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
}, },
{ {
"kind": "Content", "kind": "Content",
@ -6064,8 +6065,9 @@
"text": "index: " "text": "index: "
}, },
{ {
"kind": "Content", "kind": "Reference",
"text": "string" "text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
}, },
{ {
"kind": "Content", "kind": "Content",
@ -8299,8 +8301,9 @@
"text": "index: " "text": "index: "
}, },
{ {
"kind": "Content", "kind": "Reference",
"text": "string" "text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
}, },
{ {
"kind": "Content", "kind": "Content",

View file

@ -5,7 +5,7 @@ import {
StoreSchemaOptions, StoreSchemaOptions,
StoreSnapshot, StoreSnapshot,
} from '@tldraw/store' } from '@tldraw/store'
import { annotateError, structuredClone } from '@tldraw/utils' import { IndexKey, annotateError, structuredClone } from '@tldraw/utils'
import { CameraRecordType, TLCameraId } from './records/TLCamera' import { CameraRecordType, TLCameraId } from './records/TLCamera'
import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument' import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument'
import { TLINSTANCE_ID } from './records/TLInstance' import { TLINSTANCE_ID } from './records/TLInstance'
@ -81,7 +81,12 @@ export const onValidationFailure: StoreSchemaOptions<
function getDefaultPages() { function getDefaultPages() {
return [ return [
PageRecordType.create({ id: 'page:page' as TLPageId, name: 'Page 1', index: 'a1', meta: {} }), PageRecordType.create({
id: 'page:page' as TLPageId,
name: 'Page 1',
index: 'a1' as IndexKey,
meta: {},
}),
] ]
} }

View file

@ -1,3 +1,4 @@
import { IndexKey } from '@tldraw/utils'
import { T } from '@tldraw/validate' import { T } from '@tldraw/validate'
import { SetValue } from '../util-types' import { SetValue } from '../util-types'
@ -24,7 +25,7 @@ export interface TLHandle {
type: TLHandleType type: TLHandleType
canBind?: boolean canBind?: boolean
canSnap?: boolean canSnap?: boolean
index: string index: IndexKey
x: number x: number
y: number y: number
} }
@ -35,7 +36,7 @@ export const handleValidator: T.Validator<TLHandle> = T.object({
type: T.setEnum(TL_HANDLE_TYPES), type: T.setEnum(TL_HANDLE_TYPES),
canBind: T.boolean.optional(), canBind: T.boolean.optional(),
canSnap: T.boolean.optional(), canSnap: T.boolean.optional(),
index: T.string, index: T.indexKey,
x: T.number, x: T.number,
y: T.number, y: T.number,
}) })

View file

@ -1,5 +1,5 @@
import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store' import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store'
import { JsonObject } from '@tldraw/utils' import { IndexKey, JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate' import { T } from '@tldraw/validate'
import { idValidator } from '../misc/id-validator' import { idValidator } from '../misc/id-validator'
@ -10,7 +10,7 @@ import { idValidator } from '../misc/id-validator'
*/ */
export interface TLPage extends BaseRecord<'page', TLPageId> { export interface TLPage extends BaseRecord<'page', TLPageId> {
name: string name: string
index: string index: IndexKey
meta: JsonObject meta: JsonObject
} }
@ -27,7 +27,7 @@ export const pageValidator: T.Validator<TLPage> = T.model(
typeName: T.literal('page'), typeName: T.literal('page'),
id: pageIdValidator, id: pageIdValidator,
name: T.string, name: T.string,
index: T.string, index: T.indexKey,
meta: T.jsonValue as T.ObjectValidator<JsonObject>, meta: T.jsonValue as T.ObjectValidator<JsonObject>,
}) })
) )

View file

@ -1,5 +1,5 @@
import { BaseRecord } from '@tldraw/store' import { BaseRecord } from '@tldraw/store'
import { Expand, JsonObject } from '@tldraw/utils' import { Expand, IndexKey, JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate' import { T } from '@tldraw/validate'
import { TLOpacityType, opacityValidator } from '../misc/TLOpacity' import { TLOpacityType, opacityValidator } from '../misc/TLOpacity'
import { idValidator } from '../misc/id-validator' import { idValidator } from '../misc/id-validator'
@ -12,7 +12,7 @@ export interface TLBaseShape<Type extends string, Props extends object>
x: number x: number
y: number y: number
rotation: number rotation: number
index: string index: IndexKey
parentId: TLParentId parentId: TLParentId
isLocked: boolean isLocked: boolean
opacity: TLOpacityType opacity: TLOpacityType
@ -47,7 +47,7 @@ export function createShapeValidator<
x: T.number, x: T.number,
y: T.number, y: T.number,
rotation: T.number, rotation: T.number,
index: T.string, index: T.indexKey,
parentId: parentIdValidator, parentId: parentIdValidator,
type: T.literal(type), type: T.literal(type),
isLocked: T.boolean, isLocked: T.boolean,

View file

@ -11,6 +11,7 @@ import {
} from '@tldraw/store' } from '@tldraw/store'
import { DocumentRecordType, PageRecordType, TLDOCUMENT_ID } from '@tldraw/tlschema' import { DocumentRecordType, PageRecordType, TLDOCUMENT_ID } from '@tldraw/tlschema'
import { import {
IndexKey,
Result, Result,
assertExists, assertExists,
exhaustiveSwitchError, exhaustiveSwitchError,
@ -234,7 +235,7 @@ export class TLSyncRoom<R extends UnknownRecord> {
lastChangedClock: 0, lastChangedClock: 0,
}, },
{ {
state: PageRecordType.create({ name: 'Page 1', index: 'a1' }), state: PageRecordType.create({ name: 'Page 1', index: 'a1' as IndexKey }),
lastChangedClock: 0, lastChangedClock: 0,
}, },
], ],

View file

@ -11,7 +11,7 @@ import {
TLShapeId, TLShapeId,
createTLSchema, createTLSchema,
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { sortById } from '@tldraw/utils' import { ZERO_INDEX_KEY, sortById } from '@tldraw/utils'
import { import {
MAX_TOMBSTONES, MAX_TOMBSTONES,
RoomSnapshot, RoomSnapshot,
@ -24,7 +24,7 @@ const compareById = (a: { id: string }, b: { id: string }) => a.id.localeCompare
const records = [ const records = [
DocumentRecordType.create({}), DocumentRecordType.create({}),
PageRecordType.create({ index: 'a0', name: 'page 2' }), PageRecordType.create({ index: ZERO_INDEX_KEY, name: 'page 2' }),
].sort(compareById) ].sort(compareById)
const makeSnapshot = (records: TLRecord[], others: Partial<RoomSnapshot> = {}) => ({ const makeSnapshot = (records: TLRecord[], others: Partial<RoomSnapshot> = {}) => ({
@ -37,7 +37,7 @@ const oldArrow: TLBaseShape<'arrow', Omit<TLArrowShapeProps, 'labelColor'>> = {
typeName: 'shape', typeName: 'shape',
type: 'arrow', type: 'arrow',
id: 'shape:old_arrow' as TLShapeId, id: 'shape:old_arrow' as TLShapeId,
index: 'a0', index: ZERO_INDEX_KEY,
isLocked: false, isLocked: false,
parentId: PageRecordType.createId(), parentId: PageRecordType.createId(),
rotation: 0, rotation: 0,

View file

@ -77,6 +77,27 @@ export function getHashForObject(obj: any): string;
// @public // @public
export function getHashForString(string: string): string; export function getHashForString(string: string): string;
// @public
export function getIndexAbove(below: IndexKey): IndexKey;
// @public
export function getIndexBelow(above: IndexKey): IndexKey;
// @public
export function getIndexBetween(below: IndexKey, above?: IndexKey): IndexKey;
// @public
export function getIndices(n: number, start?: IndexKey): IndexKey[];
// @public
export function getIndicesAbove(below: IndexKey, n: number): IndexKey[];
// @public
export function getIndicesBelow(above: IndexKey, n: number): IndexKey[];
// @public
export function getIndicesBetween(below: IndexKey | undefined, above: IndexKey | undefined, n: number): IndexKey[];
// @internal (undocumented) // @internal (undocumented)
export function getOwnProperty<K extends string, V>(obj: Partial<Record<K, V>>, key: K): undefined | V; export function getOwnProperty<K extends string, V>(obj: Partial<Record<K, V>>, key: K): undefined | V;
@ -86,6 +107,11 @@ export function getOwnProperty(obj: object, key: string): unknown;
// @internal (undocumented) // @internal (undocumented)
export function hasOwnProperty(obj: object, key: string): boolean; export function hasOwnProperty(obj: object, key: string): boolean;
// @public
export type IndexKey = string & {
__orderKey: true;
};
// @public // @public
export function invLerp(a: number, b: number, t: number): number; export function invLerp(a: number, b: number, t: number): number;
@ -252,6 +278,11 @@ export function sortById<T extends {
id: any; id: any;
}>(a: T, b: T): -1 | 1; }>(a: T, b: T): -1 | 1;
// @public
export function sortByIndex<T extends {
index: IndexKey;
}>(a: T, b: T): -1 | 0 | 1;
// @public (undocumented) // @public (undocumented)
const structuredClone_2: <T>(i: T) => T; const structuredClone_2: <T>(i: T) => T;
export { structuredClone_2 as structuredClone } export { structuredClone_2 as structuredClone }
@ -262,9 +293,15 @@ export function throttle<T extends (...args: any) => any>(func: T, limit: number
// @internal // @internal
export function throttledRaf(fn: () => void): void; export function throttledRaf(fn: () => void): void;
// @internal (undocumented)
export function validateIndexKey(key: string): asserts key is IndexKey;
// @internal (undocumented) // @internal (undocumented)
export function warnDeprecatedGetter(name: string): void; export function warnDeprecatedGetter(name: string): void;
// @public
export const ZERO_INDEX_KEY: IndexKey;
// (No @packageDocumentation comment for this package) // (No @packageDocumentation comment for this package)
``` ```

View file

@ -790,6 +790,483 @@
], ],
"name": "getHashForString" "name": "getHashForString"
}, },
{
"kind": "Function",
"canonicalReference": "@tldraw/utils!getIndexAbove:function(1)",
"docComment": "/**\n * Get the index above a given index.\n *\n * @param below - The index below.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndexAbove(below: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/utils/src/lib/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "below",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"name": "getIndexAbove"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/utils!getIndexBelow:function(1)",
"docComment": "/**\n * Get the index below a given index.\n *\n * @param above - The index above.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndexBelow(above: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/utils/src/lib/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "above",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"name": "getIndexBelow"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/utils!getIndexBetween:function(1)",
"docComment": "/**\n * Get the index between two indices.\n *\n * @param below - The index below.\n *\n * @param above - The index above.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndexBetween(below: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ", above?: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/utils/src/lib/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "below",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "above",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": true
}
],
"name": "getIndexBetween"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/utils!getIndices:function(1)",
"docComment": "/**\n * Get n number of indices, starting at an index.\n *\n * @param n - The number of indices to get.\n *\n * @param start - The index to start at.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndices(n: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ", start?: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/utils/src/lib/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 7
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "n",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "start",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": true
}
],
"name": "getIndices"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/utils!getIndicesAbove:function(1)",
"docComment": "/**\n * Get a number of indices above an index.\n *\n * @param below - The index below.\n *\n * @param n - The number of indices to get.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndicesAbove(below: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ", n: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/utils/src/lib/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 7
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "below",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "n",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
}
],
"name": "getIndicesAbove"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/utils!getIndicesBelow:function(1)",
"docComment": "/**\n * Get a number of indices below an index.\n *\n * @param above - The index above.\n *\n * @param n - The number of indices to get.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndicesBelow(above: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ", n: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/utils/src/lib/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 7
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "above",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "n",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
}
],
"name": "getIndicesBelow"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/utils!getIndicesBetween:function(1)",
"docComment": "/**\n * Get a number of indices between two indices.\n *\n * @param below - The index below.\n *\n * @param above - The index above.\n *\n * @param n - The number of indices to get.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getIndicesBetween(below: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": " | undefined"
},
{
"kind": "Content",
"text": ", above: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": " | undefined"
},
{
"kind": "Content",
"text": ", n: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/utils/src/lib/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 9,
"endIndex": 11
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "below",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isOptional": false
},
{
"parameterName": "above",
"parameterTypeTokenRange": {
"startIndex": 4,
"endIndex": 6
},
"isOptional": false
},
{
"parameterName": "n",
"parameterTypeTokenRange": {
"startIndex": 7,
"endIndex": 8
},
"isOptional": false
}
],
"name": "getIndicesBetween"
},
{
"kind": "TypeAlias",
"canonicalReference": "@tldraw/utils!IndexKey:type",
"docComment": "/**\n * A string made up of an integer part followed by a fraction part. The fraction point consists of zero or more digits with no trailing zeros. Based on {@link https://observablehq.com/@dgreensp/implementing-fractional-indexing}.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export type IndexKey = "
},
{
"kind": "Content",
"text": "string & {\n __orderKey: true;\n}"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/utils/src/lib/reordering/IndexKey.ts",
"releaseTag": "Public",
"name": "IndexKey",
"typeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
},
{ {
"kind": "Function", "kind": "Function",
"canonicalReference": "@tldraw/utils!invLerp:function(1)", "canonicalReference": "@tldraw/utils!invLerp:function(1)",
@ -2666,6 +3143,97 @@
], ],
"name": "sortById" "name": "sortById"
}, },
{
"kind": "Function",
"canonicalReference": "@tldraw/utils!sortByIndex:function(1)",
"docComment": "/**\n * Sort by index.\n *\n * @param a - An object with an index property.\n *\n * @param b - An object with an index property.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function sortByIndex<T extends "
},
{
"kind": "Content",
"text": "{\n index: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ";\n}"
},
{
"kind": "Content",
"text": ">(a: "
},
{
"kind": "Content",
"text": "T"
},
{
"kind": "Content",
"text": ", b: "
},
{
"kind": "Content",
"text": "T"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "-1 | 0 | 1"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/utils/src/lib/reordering/reordering.ts",
"returnTypeTokenRange": {
"startIndex": 9,
"endIndex": 10
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "a",
"parameterTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"isOptional": false
},
{
"parameterName": "b",
"parameterTypeTokenRange": {
"startIndex": 7,
"endIndex": 8
},
"isOptional": false
}
],
"typeParameters": [
{
"typeParameterName": "T",
"constraintTokenRange": {
"startIndex": 1,
"endIndex": 4
},
"defaultTypeTokenRange": {
"startIndex": 0,
"endIndex": 0
}
}
],
"name": "sortByIndex"
},
{ {
"kind": "Variable", "kind": "Variable",
"canonicalReference": "@tldraw/utils!structuredClone_2:var", "canonicalReference": "@tldraw/utils!structuredClone_2:var",
@ -2788,6 +3356,30 @@
} }
], ],
"name": "throttle" "name": "throttle"
},
{
"kind": "Variable",
"canonicalReference": "@tldraw/utils!ZERO_INDEX_KEY:var",
"docComment": "/**\n * The index key for the first index - 'a0'.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "ZERO_INDEX_KEY: "
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
}
],
"fileUrlPath": "packages/utils/src/lib/reordering/reordering.ts",
"isReadonly": true,
"releaseTag": "Public",
"name": "ZERO_INDEX_KEY",
"variableTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
} }
] ]
} }

View file

@ -39,6 +39,19 @@ export {
} from './lib/object' } from './lib/object'
export { PngHelpers } from './lib/png' export { PngHelpers } from './lib/png'
export { rafThrottle, throttledRaf } from './lib/raf' export { rafThrottle, throttledRaf } from './lib/raf'
export { type IndexKey } from './lib/reordering/IndexKey'
export {
ZERO_INDEX_KEY,
getIndexAbove,
getIndexBelow,
getIndexBetween,
getIndices,
getIndicesAbove,
getIndicesBelow,
getIndicesBetween,
sortByIndex,
validateIndexKey,
} from './lib/reordering/reordering'
export { sortById } from './lib/sort' export { sortById } from './lib/sort'
export type { Expand, RecursivePartial, Required } from './lib/types' export type { Expand, RecursivePartial, Required } from './lib/types'
export { isDefined, isNonNull, isNonNullish, structuredClone } from './lib/value' export { isDefined, isNonNull, isNonNullish, structuredClone } from './lib/value'

View file

@ -0,0 +1,8 @@
/**
* A string made up of an integer part followed by a fraction part. The fraction point consists of
* zero or more digits with no trailing zeros. Based on
* {@link https://observablehq.com/@dgreensp/implementing-fractional-indexing}.
*
* @public
*/
export type IndexKey = string & { __orderKey: true }

View file

@ -1,6 +1,8 @@
import { IndexKey } from '../IndexKey'
import { generateNKeysBetween } from './dgreensp' import { generateNKeysBetween } from './dgreensp'
const generateKeyBetween = (a?: string, b?: string) => generateNKeysBetween(a, b, 1)[0] const generateKeyBetween = (a?: string, b?: string) =>
generateNKeysBetween(a as IndexKey, b as IndexKey, 1)[0]
describe('get order between', () => { describe('get order between', () => {
it('passes tests', () => { it('passes tests', () => {
@ -40,11 +42,11 @@ describe('get order between', () => {
describe('generateNKeysBetween', () => { describe('generateNKeysBetween', () => {
it('Gets the correct orders between', () => { it('Gets the correct orders between', () => {
expect(generateNKeysBetween(undefined, undefined, 5).join(' ')).toBe('a0 a1 a2 a3 a4') expect(generateNKeysBetween(undefined, undefined, 5).join(' ')).toBe('a0 a1 a2 a3 a4')
expect(generateNKeysBetween('a4', undefined, 10).join(' ')).toBe( expect(generateNKeysBetween('a4' as IndexKey, undefined, 10).join(' ')).toBe(
'a5 a6 a7 a8 a9 aA aB aC aD aE' 'a5 a6 a7 a8 a9 aA aB aC aD aE'
) )
expect(generateNKeysBetween(undefined, 'a0', 5).join(' ')).toBe('Zv Zw Zx Zy Zz') expect(generateNKeysBetween(undefined, 'a0' as IndexKey, 5).join(' ')).toBe('Zv Zw Zx Zy Zz')
expect(generateNKeysBetween('a0', 'a2', 20).join(' ')).toBe( expect(generateNKeysBetween('a0' as IndexKey, 'a2' as IndexKey, 20).join(' ')).toBe(
'a04 a08 a0G a0K a0O a0V a0Z a0d a0l a0t a1 a14 a18 a1G a1O a1V a1Z a1d a1l a1t' 'a04 a08 a0G a0K a0O a0V a0Z a0d a0l a0t a1 a14 a18 a1G a1O a1V a1Z a1d a1l a1t'
) )
}) })

View file

@ -1,8 +1,10 @@
// Adapted from https://observablehq.com/@dgreensp/implementing-fractional-indexing // Adapted from https://observablehq.com/@dgreensp/implementing-fractional-indexing
// by @dgreensp (twitter @DavidLG) // by @dgreensp (twitter @DavidLG)
import { IndexKey } from '../IndexKey'
const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
const INTEGER_ZERO = 'a0' export const INTEGER_ZERO = 'a0' as IndexKey
const SMALLEST_INTEGER = 'A00000000000000000000000000' const SMALLEST_INTEGER = 'A00000000000000000000000000'
/** /**
@ -161,7 +163,7 @@ function getIntegerPart(index: string): string {
* *
* @param x - The index to validate. * @param x - The index to validate.
*/ */
function validateOrder(index: string): asserts index is string { export function validateOrder(index: string): asserts index is string {
if (index === SMALLEST_INTEGER) { if (index === SMALLEST_INTEGER) {
throw new Error('invalid index: ' + index) throw new Error('invalid index: ' + index)
} }
@ -175,19 +177,13 @@ function validateOrder(index: string): asserts index is string {
} }
} }
/**
* A string made up of an integer part followed by a fraction part. The fraction point consists of
* zero or more digits with no trailing zeros.
*/
type OrderKey = string
/** /**
* Generate an index key at the midpoint between a start and end. * Generate an index key at the midpoint between a start and end.
* *
* @param a - The start index key string. * @param a - The start index key string.
* @param b - The end index key string, greater than A. * @param b - The end index key string, greater than A.
*/ */
function generateKeyBetween(a: OrderKey | undefined, b: OrderKey | undefined): OrderKey { function generateKeyBetween(a: IndexKey | undefined, b: IndexKey | undefined): IndexKey {
if (a !== undefined) validateOrder(a) if (a !== undefined) validateOrder(a)
if (b !== undefined) validateOrder(b) if (b !== undefined) validateOrder(b)
if (a !== undefined && b !== undefined && a >= b) { if (a !== undefined && b !== undefined && a >= b) {
@ -201,31 +197,31 @@ function generateKeyBetween(a: OrderKey | undefined, b: OrderKey | undefined): O
const ib = getIntegerPart(b) const ib = getIntegerPart(b)
const fb = b.slice(ib.length) const fb = b.slice(ib.length)
if (ib === SMALLEST_INTEGER) { if (ib === SMALLEST_INTEGER) {
return ib + midpoint('', fb) return (ib + midpoint('', fb)) as IndexKey
} }
if (ib < b) { if (ib < b) {
return ib return ib as IndexKey
} }
const ibl = decrementInteger(ib) const ibl = decrementInteger(ib)
isNotUndefined(ibl) isNotUndefined(ibl)
return ibl return ibl as IndexKey
} }
if (b === undefined) { if (b === undefined) {
const ia = getIntegerPart(a) const ia = getIntegerPart(a)
const fa = a.slice(ia.length) const fa = a.slice(ia.length)
const i = incrementInteger(ia) const i = incrementInteger(ia)
return i === undefined ? ia + midpoint(fa, undefined) : i return (i === undefined ? ia + midpoint(fa, undefined) : i) as IndexKey
} }
const ia = getIntegerPart(a) const ia = getIntegerPart(a)
const fa = a.slice(ia.length) const fa = a.slice(ia.length)
const ib = getIntegerPart(b) const ib = getIntegerPart(b)
const fb = b.slice(ib.length) const fb = b.slice(ib.length)
if (ia === ib) { if (ia === ib) {
return ia + midpoint(fa, fb) return (ia + midpoint(fa, fb)) as IndexKey
} }
const i = incrementInteger(ia) const i = incrementInteger(ia)
isNotUndefined(i) isNotUndefined(i)
return i < b ? i : ia + midpoint(fa, undefined) return (i < b ? i : ia + midpoint(fa, undefined)) as IndexKey
} }
/** /**
@ -236,10 +232,10 @@ function generateKeyBetween(a: OrderKey | undefined, b: OrderKey | undefined): O
* @param n - The number of index keys to generate. * @param n - The number of index keys to generate.
*/ */
export function generateNKeysBetween( export function generateNKeysBetween(
a: string | undefined, a: IndexKey | undefined,
b: string | undefined, b: IndexKey | undefined,
n: number n: number
): string[] { ): IndexKey[] {
if (n === 0) return [] if (n === 0) return []
if (n === 1) return [generateKeyBetween(a, b)] if (n === 1) return [generateKeyBetween(a, b)]
if (b === undefined) { if (b === undefined) {

View file

@ -1,4 +1,16 @@
import { generateNKeysBetween } from './dgreensp/dgreensp' import { IndexKey } from './IndexKey'
import { INTEGER_ZERO, generateNKeysBetween, validateOrder } from './dgreensp/dgreensp'
/**
* The index key for the first index - 'a0'.
* @public
*/
export const ZERO_INDEX_KEY = INTEGER_ZERO
/** @internal */
export function validateIndexKey(key: string): asserts key is IndexKey {
validateOrder(key)
}
/** /**
* Get a number of indices between two indices. * Get a number of indices between two indices.
@ -7,7 +19,11 @@ import { generateNKeysBetween } from './dgreensp/dgreensp'
* @param n - The number of indices to get. * @param n - The number of indices to get.
* @public * @public
*/ */
export function getIndicesBetween(below: string | undefined, above: string | undefined, n: number) { export function getIndicesBetween(
below: IndexKey | undefined,
above: IndexKey | undefined,
n: number
) {
return generateNKeysBetween(below, above, n) return generateNKeysBetween(below, above, n)
} }
@ -17,7 +33,7 @@ export function getIndicesBetween(below: string | undefined, above: string | und
* @param n - The number of indices to get. * @param n - The number of indices to get.
* @public * @public
*/ */
export function getIndicesAbove(below: string, n: number) { export function getIndicesAbove(below: IndexKey, n: number) {
return generateNKeysBetween(below, undefined, n) return generateNKeysBetween(below, undefined, n)
} }
@ -27,7 +43,7 @@ export function getIndicesAbove(below: string, n: number) {
* @param n - The number of indices to get. * @param n - The number of indices to get.
* @public * @public
*/ */
export function getIndicesBelow(above: string, n: number) { export function getIndicesBelow(above: IndexKey, n: number) {
return generateNKeysBetween(undefined, above, n) return generateNKeysBetween(undefined, above, n)
} }
@ -37,7 +53,7 @@ export function getIndicesBelow(above: string, n: number) {
* @param above - The index above. * @param above - The index above.
* @public * @public
*/ */
export function getIndexBetween(below: string, above?: string) { export function getIndexBetween(below: IndexKey, above?: IndexKey) {
return generateNKeysBetween(below, above, 1)[0] return generateNKeysBetween(below, above, 1)[0]
} }
@ -46,7 +62,7 @@ export function getIndexBetween(below: string, above?: string) {
* @param below - The index below. * @param below - The index below.
* @public * @public
*/ */
export function getIndexAbove(below: string) { export function getIndexAbove(below: IndexKey) {
return generateNKeysBetween(below, undefined, 1)[0] return generateNKeysBetween(below, undefined, 1)[0]
} }
@ -55,7 +71,7 @@ export function getIndexAbove(below: string) {
* @param above - The index above. * @param above - The index above.
* @public * @public
*/ */
export function getIndexBelow(above: string) { export function getIndexBelow(above: IndexKey) {
return generateNKeysBetween(undefined, above, 1)[0] return generateNKeysBetween(undefined, above, 1)[0]
} }
@ -65,7 +81,7 @@ export function getIndexBelow(above: string) {
* @param start - The index to start at. * @param start - The index to start at.
* @public * @public
*/ */
export function getIndices(n: number, start = 'a1') { export function getIndices(n: number, start = 'a1' as IndexKey) {
return [start, ...generateNKeysBetween(start, undefined, n)] return [start, ...generateNKeysBetween(start, undefined, n)]
} }
@ -74,7 +90,7 @@ export function getIndices(n: number, start = 'a1') {
* @param a - An object with an index property. * @param a - An object with an index property.
* @param b - An object with an index property. * @param b - An object with an index property.
* @public */ * @public */
export function sortByIndex<T extends { index: string }>(a: T, b: T) { export function sortByIndex<T extends { index: IndexKey }>(a: T, b: T) {
if (a.index < b.index) { if (a.index < b.index) {
return -1 return -1
} else if (a.index > b.index) { } else if (a.index > b.index) {

View file

@ -4,6 +4,7 @@
```ts ```ts
import { IndexKey } from '@tldraw/utils';
import { JsonValue } from '@tldraw/utils'; import { JsonValue } from '@tldraw/utils';
// @public // @public
@ -44,6 +45,9 @@ export class DictValidator<Key extends string, Value> extends Validator<Record<K
readonly valueValidator: Validatable<Value>; readonly valueValidator: Validatable<Value>;
} }
// @public
const indexKey: Validator<IndexKey>;
// @public // @public
const integer: Validator<number>; const integer: Validator<number>;
@ -155,7 +159,8 @@ declare namespace T {
unknownObject, unknownObject,
jsonValue, jsonValue,
linkUrl, linkUrl,
srcUrl srcUrl,
indexKey
} }
} }
export { T } export { T }

View file

@ -1543,6 +1543,43 @@
}, },
"implementsTokenRanges": [] "implementsTokenRanges": []
}, },
{
"kind": "Variable",
"canonicalReference": "@tldraw/validate!T.indexKey:var",
"docComment": "/**\n * Validates that a value is an IndexKey.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "indexKey: "
},
{
"kind": "Reference",
"text": "Validator",
"canonicalReference": "@tldraw/validate!Validator:class"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
"text": ">"
}
],
"fileUrlPath": "packages/validate/src/lib/validation.ts",
"isReadonly": true,
"releaseTag": "Public",
"name": "indexKey",
"variableTypeTokenRange": {
"startIndex": 1,
"endIndex": 5
}
},
{ {
"kind": "Variable", "kind": "Variable",
"canonicalReference": "@tldraw/validate!T.integer:var", "canonicalReference": "@tldraw/validate!T.integer:var",

View file

@ -1,4 +1,11 @@
import { JsonValue, exhaustiveSwitchError, getOwnProperty, hasOwnProperty } from '@tldraw/utils' import {
IndexKey,
JsonValue,
exhaustiveSwitchError,
getOwnProperty,
hasOwnProperty,
validateIndexKey,
} from '@tldraw/utils'
/** @public */ /** @public */
export type ValidatorFn<T> = (value: unknown) => T export type ValidatorFn<T> = (value: unknown) => T
@ -668,3 +675,16 @@ export const srcUrl = string.check((value) => {
) )
} }
}) })
/**
* Validates that a value is an IndexKey.
* @public
*/
export const indexKey = string.refine<IndexKey>((key) => {
try {
validateIndexKey(key)
return key
} catch {
throw new ValidationError(`Expected an index key, got ${JSON.stringify(key)}`)
}
})

View file

@ -122,3 +122,18 @@ describe('T.refine', () => {
describe('T.check', () => { describe('T.check', () => {
it.todo('Adds a check to a validator.') it.todo('Adds a check to a validator.')
}) })
describe('T.indexKey', () => {
it('validates index keys', () => {
expect(T.indexKey.validate('a0')).toBe('a0')
expect(T.indexKey.validate('a1J')).toBe('a1J')
})
it('rejects invalid index keys', () => {
expect(() => T.indexKey.validate('a')).toThrowErrorMatchingInlineSnapshot(
`"At null: Expected an index key, got "a""`
)
expect(() => T.indexKey.validate('')).toThrowErrorMatchingInlineSnapshot(
`"At null: Expected an index key, got """`
)
})
})