[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

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

View file

@ -10585,8 +10585,9 @@
"text": "): "
},
{
"kind": "Content",
"text": "string"
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
@ -15742,8 +15743,9 @@
"text": ", insertIndex?: "
},
{
"kind": "Content",
"text": "string"
"kind": "Reference",
"text": "IndexKey",
"canonicalReference": "@tldraw/utils!IndexKey:type"
},
{
"kind": "Content",
@ -21639,417 +21641,6 @@
],
"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",
"canonicalReference": "@tldraw/editor!getPointerInfo:function(1)",
@ -32799,88 +32390,6 @@
],
"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",
"canonicalReference": "@tldraw/editor!Stadium2d:class",

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
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 { getIndicesBetween, sortByIndex } from './reordering/reordering'
export function getReorderingShapesChanges(
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 (moving.size === len) return
let below: string | undefined
let above: string | undefined
let below: IndexKey | undefined
let above: IndexKey | undefined
// Starting at the bottom of this parent's children...
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 (moving.size === len) return
let below: string | undefined
let above: string | undefined
let below: IndexKey | undefined
let above: IndexKey | undefined
// Starting at the top of this parent's children...
for (let i = len - 1; i > -1; i--) {

View file

@ -1,5 +1,5 @@
import { PageRecordType } from '@tldraw/tlschema'
import { promiseWithResolve } from '@tldraw/utils'
import { IndexKey, promiseWithResolve } from '@tldraw/utils'
import { createTLStore } from '../../config/createTLStore'
import { TLLocalSyncClient } from './TLLocalSyncClient'
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 () => {
const { client } = testClient()
await tick()
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' })])
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' as IndexKey })])
await tick()
expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1)
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()
expect(idb.storeSnapshotInIndexedDb).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 () => {
const { client } = testClient()
await tick()
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' })])
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' as IndexKey })])
await tick()
// @ts-expect-error
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()
// @ts-expect-error
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()
await tick()
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' })])
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' as IndexKey })])
await tick()
// 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)
// 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()
expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1)
expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(0)

View file

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

View file

@ -7342,7 +7342,16 @@
},
{
"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",
@ -7382,7 +7391,7 @@
"name": "onBeforeCreate",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 10
"endIndex": 12
},
"isStatic": false,
"isProtected": false,
@ -7417,7 +7426,16 @@
},
{
"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",
@ -7457,7 +7475,7 @@
"name": "onBeforeUpdate",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 12
"endIndex": 14
},
"isStatic": false,
"isProtected": false,
@ -7483,7 +7501,16 @@
},
{
"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",
@ -7510,7 +7537,16 @@
},
{
"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",
@ -7550,7 +7586,7 @@
"name": "onDoubleClick",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 16
"endIndex": 20
},
"isStatic": false,
"isProtected": false,
@ -11997,7 +12033,16 @@
},
{
"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",
@ -12037,7 +12082,7 @@
"name": "onBeforeCreate",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 10
"endIndex": 12
},
"isStatic": false,
"isProtected": false,
@ -12072,7 +12117,16 @@
},
{
"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",
@ -12112,7 +12166,7 @@
"name": "onBeforeUpdate",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 12
"endIndex": 14
},
"isStatic": false,
"isProtected": false,
@ -13660,7 +13714,16 @@
},
{
"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",
@ -13700,7 +13763,7 @@
"name": "onBeforeCreate",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 10
"endIndex": 12
},
"isStatic": false,
"isProtected": false,
@ -13735,7 +13798,16 @@
},
{
"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",
@ -13775,7 +13847,7 @@
"name": "onBeforeUpdate",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 12
"endIndex": 14
},
"isStatic": 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'
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
])
editor.expectShapeToMatch({ id: ids.box1, index: 'a2' })
editor.expectShapeToMatch({ id: ids.box2, index: 'a3' })
editor.expectShapeToMatch({ id: ids.box3, index: 'a4' })
editor.expectShapeToMatch({ id: ids.box1, index: 'a2' as IndexKey })
editor.expectShapeToMatch({ id: ids.box2, index: 'a3' as IndexKey })
editor.expectShapeToMatch({ id: ids.box3, index: 'a4' as IndexKey })
editor.select(arrowId)
editor.pointerDown(100, 100, {
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)!,
})
editor.expectToBeIn('select.pointing_handle')
@ -422,7 +422,7 @@ describe('reparenting issue', () => {
editor.expectToBeIn('select.dragging_handle')
editor.expectShapeToMatch({
id: arrowId,
index: 'a3V',
index: 'a3V' as IndexKey,
props: { end: { boundShapeId: ids.box2 } },
}) // 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.expectShapeToMatch({
id: arrowId,
index: 'a5',
index: 'a5' as IndexKey,
props: { end: { boundShapeId: ids.box3 } },
}) // above box 3 (a4)
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.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?
})
@ -481,7 +481,7 @@ describe('reparenting issue', () => {
.select(arrow1Id)
.pointerDown(100, 100, {
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)!,
})
.pointerMove(120, 120)
@ -490,7 +490,7 @@ describe('reparenting issue', () => {
.select(arrow2Id)
.pointerDown(100, 100, {
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)!,
})
.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'
jest.mock('nanoid', () => {
@ -85,7 +85,7 @@ it('create new handle', () => {
handle: {
id: 'mid-0',
type: 'create',
index: 'a1V',
index: 'a1V' as IndexKey,
x: 50,
y: 50,
},

View file

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

View file

@ -1,4 +1,5 @@
import {
IndexKey,
Mat,
StateNode,
TLEventHandlers,
@ -50,7 +51,7 @@ export class Pointing extends StateNode {
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)

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) => {
let index: string
let index: IndexKey
const pages = editor.getPages()

View file

@ -15,6 +15,7 @@ import {
TLShapeId,
Vec,
VecLike,
ZERO_INDEX_KEY,
compact,
createShapeId,
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) {
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'
let editor: TestEditor
@ -114,7 +114,7 @@ describe('PointingHandle', () => {
editor.pointerDown(150, 150, {
target: 'handle',
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')
@ -127,7 +127,7 @@ describe('PointingHandle', () => {
editor.pointerDown(150, 150, {
target: 'handle',
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.cancel()
@ -142,7 +142,7 @@ describe('DraggingHandle', () => {
editor.pointerDown(150, 150, {
target: 'handle',
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.expectToBeIn('select.dragging_handle')
@ -158,7 +158,7 @@ describe('DraggingHandle', () => {
editor.pointerDown(150, 150, {
target: 'handle',
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.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'
let editor: TestEditor
@ -122,7 +122,7 @@ describe('Editor.moveShapesToPage', () => {
editor.expectShapeToMatch({
id: ids.box1,
parentId: page2Id,
index: 'a1',
index: 'a1' as IndexKey,
})
const page3Id = PageRecordType.createId('newPage3')
@ -134,7 +134,7 @@ describe('Editor.moveShapesToPage', () => {
editor.expectShapeToMatch({
id: ids.box2,
parentId: page3Id,
index: 'a1',
index: 'a1' as IndexKey,
})
editor.setCurrentPage(page2Id)
@ -147,12 +147,12 @@ describe('Editor.moveShapesToPage', () => {
{
id: ids.box2,
parentId: page3Id,
index: 'a1',
index: 'a1' as IndexKey,
},
{
id: ids.box1,
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'
let editor: TestEditor
@ -105,7 +105,7 @@ it('adds children at a given index', () => {
expect(editor.getShape(ids.ellipse1)!.index).toBe('a1')
// 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
// - 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)
// 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
// - 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)
// 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
// - 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'
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", () => {
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()
editor.store.put([page])
expect(editor.getPageStates().find((p) => p.pageId === page.id)).not.toBeUndefined()
@ -47,7 +47,7 @@ describe('setCurrentPage', () => {
it('squashes', () => {
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.setCurrentPage(editor.getPages()[1].id)

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import {
StoreSchemaOptions,
StoreSnapshot,
} from '@tldraw/store'
import { annotateError, structuredClone } from '@tldraw/utils'
import { IndexKey, annotateError, structuredClone } from '@tldraw/utils'
import { CameraRecordType, TLCameraId } from './records/TLCamera'
import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument'
import { TLINSTANCE_ID } from './records/TLInstance'
@ -81,7 +81,12 @@ export const onValidationFailure: StoreSchemaOptions<
function getDefaultPages() {
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 { SetValue } from '../util-types'
@ -24,7 +25,7 @@ export interface TLHandle {
type: TLHandleType
canBind?: boolean
canSnap?: boolean
index: string
index: IndexKey
x: number
y: number
}
@ -35,7 +36,7 @@ export const handleValidator: T.Validator<TLHandle> = T.object({
type: T.setEnum(TL_HANDLE_TYPES),
canBind: T.boolean.optional(),
canSnap: T.boolean.optional(),
index: T.string,
index: T.indexKey,
x: T.number,
y: T.number,
})

View file

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

View file

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

View file

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

View file

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

View file

@ -77,6 +77,27 @@ export function getHashForObject(obj: any): string;
// @public
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)
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)
export function hasOwnProperty(obj: object, key: string): boolean;
// @public
export type IndexKey = string & {
__orderKey: true;
};
// @public
export function invLerp(a: number, b: number, t: number): number;
@ -252,6 +278,11 @@ export function sortById<T extends {
id: any;
}>(a: T, b: T): -1 | 1;
// @public
export function sortByIndex<T extends {
index: IndexKey;
}>(a: T, b: T): -1 | 0 | 1;
// @public (undocumented)
const structuredClone_2: <T>(i: T) => T;
export { structuredClone_2 as structuredClone }
@ -262,9 +293,15 @@ export function throttle<T extends (...args: any) => any>(func: T, limit: number
// @internal
export function throttledRaf(fn: () => void): void;
// @internal (undocumented)
export function validateIndexKey(key: string): asserts key is IndexKey;
// @internal (undocumented)
export function warnDeprecatedGetter(name: string): void;
// @public
export const ZERO_INDEX_KEY: IndexKey;
// (No @packageDocumentation comment for this package)
```

View file

@ -790,6 +790,483 @@
],
"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",
"canonicalReference": "@tldraw/utils!invLerp:function(1)",
@ -2666,6 +3143,97 @@
],
"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",
"canonicalReference": "@tldraw/utils!structuredClone_2:var",
@ -2788,6 +3356,30 @@
}
],
"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'
export { PngHelpers } from './lib/png'
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 type { Expand, RecursivePartial, Required } from './lib/types'
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'
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', () => {
it('passes tests', () => {
@ -40,11 +42,11 @@ describe('get order between', () => {
describe('generateNKeysBetween', () => {
it('Gets the correct orders between', () => {
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'
)
expect(generateNKeysBetween(undefined, 'a0', 5).join(' ')).toBe('Zv Zw Zx Zy Zz')
expect(generateNKeysBetween('a0', 'a2', 20).join(' ')).toBe(
expect(generateNKeysBetween(undefined, 'a0' as IndexKey, 5).join(' ')).toBe('Zv Zw Zx Zy Zz')
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'
)
})

View file

@ -1,8 +1,10 @@
// Adapted from https://observablehq.com/@dgreensp/implementing-fractional-indexing
// by @dgreensp (twitter @DavidLG)
import { IndexKey } from '../IndexKey'
const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
const INTEGER_ZERO = 'a0'
export const INTEGER_ZERO = 'a0' as IndexKey
const SMALLEST_INTEGER = 'A00000000000000000000000000'
/**
@ -161,7 +163,7 @@ function getIntegerPart(index: string): string {
*
* @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) {
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.
*
* @param a - The start index key string.
* @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 (b !== undefined) validateOrder(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 fb = b.slice(ib.length)
if (ib === SMALLEST_INTEGER) {
return ib + midpoint('', fb)
return (ib + midpoint('', fb)) as IndexKey
}
if (ib < b) {
return ib
return ib as IndexKey
}
const ibl = decrementInteger(ib)
isNotUndefined(ibl)
return ibl
return ibl as IndexKey
}
if (b === undefined) {
const ia = getIntegerPart(a)
const fa = a.slice(ia.length)
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 fa = a.slice(ia.length)
const ib = getIntegerPart(b)
const fb = b.slice(ib.length)
if (ia === ib) {
return ia + midpoint(fa, fb)
return (ia + midpoint(fa, fb)) as IndexKey
}
const i = incrementInteger(ia)
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.
*/
export function generateNKeysBetween(
a: string | undefined,
b: string | undefined,
a: IndexKey | undefined,
b: IndexKey | undefined,
n: number
): string[] {
): IndexKey[] {
if (n === 0) return []
if (n === 1) return [generateKeyBetween(a, b)]
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.
@ -7,7 +19,11 @@ import { generateNKeysBetween } from './dgreensp/dgreensp'
* @param n - The number of indices to get.
* @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)
}
@ -17,7 +33,7 @@ export function getIndicesBetween(below: string | undefined, above: string | und
* @param n - The number of indices to get.
* @public
*/
export function getIndicesAbove(below: string, n: number) {
export function getIndicesAbove(below: IndexKey, n: number) {
return generateNKeysBetween(below, undefined, n)
}
@ -27,7 +43,7 @@ export function getIndicesAbove(below: string, n: number) {
* @param n - The number of indices to get.
* @public
*/
export function getIndicesBelow(above: string, n: number) {
export function getIndicesBelow(above: IndexKey, n: number) {
return generateNKeysBetween(undefined, above, n)
}
@ -37,7 +53,7 @@ export function getIndicesBelow(above: string, n: number) {
* @param above - The index above.
* @public
*/
export function getIndexBetween(below: string, above?: string) {
export function getIndexBetween(below: IndexKey, above?: IndexKey) {
return generateNKeysBetween(below, above, 1)[0]
}
@ -46,7 +62,7 @@ export function getIndexBetween(below: string, above?: string) {
* @param below - The index below.
* @public
*/
export function getIndexAbove(below: string) {
export function getIndexAbove(below: IndexKey) {
return generateNKeysBetween(below, undefined, 1)[0]
}
@ -55,7 +71,7 @@ export function getIndexAbove(below: string) {
* @param above - The index above.
* @public
*/
export function getIndexBelow(above: string) {
export function getIndexBelow(above: IndexKey) {
return generateNKeysBetween(undefined, above, 1)[0]
}
@ -65,7 +81,7 @@ export function getIndexBelow(above: string) {
* @param start - The index to start at.
* @public
*/
export function getIndices(n: number, start = 'a1') {
export function getIndices(n: number, start = 'a1' as IndexKey) {
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 b - An object with an index property.
* @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) {
return -1
} else if (a.index > b.index) {

View file

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

View file

@ -1543,6 +1543,43 @@
},
"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",
"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 */
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', () => {
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 """`
)
})
})