diff --git a/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx b/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx index e283f16ee..1cf82b55c 100644 --- a/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx +++ b/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx @@ -13,6 +13,7 @@ import { TLOnHandleDragHandler, TLOnResizeHandler, Vec, + ZERO_INDEX_KEY, deepCopy, getDefaultColorTheme, resizeBox, @@ -80,7 +81,7 @@ export class SpeechBubbleUtil extends ShapeUtil { type: 'vertex', canBind: true, canSnap: true, - index: 'a1', + index: ZERO_INDEX_KEY, x: 0.5, y: 1.5, }, diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index e6f428ff4..ed0194828 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -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 { getErasingShapes(): NonNullable[]; getFocusedGroup(): TLShape | undefined; getFocusedGroupId(): TLPageId | TLShapeId; - getHighestIndexForParent(parent: TLPage | TLParentId | TLShape): string; + getHighestIndexForParent(parent: TLPage | TLParentId | TLShape): IndexKey; getHintingShape(): NonNullable[]; getHintingShapeIds(): TLShapeId[]; getHoveredShape(): TLShape | undefined; @@ -844,7 +845,7 @@ export class Editor extends EventEmitter { } : 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(a: T, b: T): -1 | 0 | 1; - // @public (undocumented) export class Stadium2d extends Ellipse2d { constructor(config: Omit & { diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 8444ec800..9835a0cbc 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -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(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", diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 1f965cd19..ac99b948c 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -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, diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 66523c2f3..0fda3bc0c 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -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 { 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 { * * @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 { 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 { * * @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 { // Get the highest index among the parents of each of the // the shapes being created; we'll increment from there. - const parentIndices = new Map() + const parentIndices = new Map() const shapeRecordsToCreate: TLShape[] = [] diff --git a/packages/editor/src/lib/editor/derivations/parentsToChildren.ts b/packages/editor/src/lib/editor/derivations/parentsToChildren.ts index 359857421..62d9d4f67 100644 --- a/packages/editor/src/lib/editor/derivations/parentsToChildren.ts +++ b/packages/editor/src/lib/editor/derivations/parentsToChildren.ts @@ -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 diff --git a/packages/editor/src/lib/utils/reorderShapes.ts b/packages/editor/src/lib/utils/reorderShapes.ts index 124acce67..9d032ebad 100644 --- a/packages/editor/src/lib/utils/reorderShapes.ts +++ b/packages/editor/src/lib/utils/reorderShapes.ts @@ -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, 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, 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--) { diff --git a/packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts b/packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts index cbc5cfaa4..591bf9f18 100644 --- a/packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts +++ b/packages/editor/src/lib/utils/sync/TLLocalSyncClient.test.ts @@ -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) diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 1f6049415..3f3f4b40a 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { }; type: "text"; rotation: number; - index: string; + index: IndexKey; parentId: TLParentId; isLocked: boolean; opacity: number; diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 94620ac9f..9e30078d9 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -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, diff --git a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeTool.test.ts b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeTool.test.ts index bb1debbad..50e698626 100644 --- a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeTool.test.ts +++ b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeTool.test.ts @@ -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) diff --git a/packages/tldraw/src/lib/shapes/line/LineShapeUtil.test.ts b/packages/tldraw/src/lib/shapes/line/LineShapeUtil.test.ts index fcb734aba..151b1bb73 100644 --- a/packages/tldraw/src/lib/shapes/line/LineShapeUtil.test.ts +++ b/packages/tldraw/src/lib/shapes/line/LineShapeUtil.test.ts @@ -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, }, diff --git a/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx b/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx index fef360338..186eaa0f8 100644 --- a/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx +++ b/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx @@ -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 { 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 { type: 'vertex', canBind: false, canSnap: true, - index: 'a2', + index: 'a2' as IndexKey, x: 0.1, y: 0.1, }, diff --git a/packages/tldraw/src/lib/shapes/line/toolStates/Pointing.ts b/packages/tldraw/src/lib/shapes/line/toolStates/Pointing.ts index 5f47393d1..0c0cbcec6 100644 --- a/packages/tldraw/src/lib/shapes/line/toolStates/Pointing.ts +++ b/packages/tldraw/src/lib/shapes/line/toolStates/Pointing.ts @@ -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) diff --git a/packages/tldraw/src/lib/ui/components/PageMenu/edit-pages-shared.ts b/packages/tldraw/src/lib/ui/components/PageMenu/edit-pages-shared.ts index df18eee47..c84eb2d00 100644 --- a/packages/tldraw/src/lib/ui/components/PageMenu/edit-pages-shared.ts +++ b/packages/tldraw/src/lib/ui/components/PageMenu/edit-pages-shared.ts @@ -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() diff --git a/packages/tldraw/src/lib/ui/hooks/clipboard/pasteExcalidrawContent.ts b/packages/tldraw/src/lib/ui/hooks/clipboard/pasteExcalidrawContent.ts index aee071666..bf8452a74 100644 --- a/packages/tldraw/src/lib/ui/hooks/clipboard/pasteExcalidrawContent.ts +++ b/packages/tldraw/src/lib/ui/hooks/clipboard/pasteExcalidrawContent.ts @@ -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)) { diff --git a/packages/tldraw/src/test/SelectTool.test.ts b/packages/tldraw/src/test/SelectTool.test.ts index 1b2f918e3..a975eb910 100644 --- a/packages/tldraw/src/test/SelectTool.test.ts +++ b/packages/tldraw/src/test/SelectTool.test.ts @@ -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') diff --git a/packages/tldraw/src/test/commands/moveShapesToPage.test.ts b/packages/tldraw/src/test/commands/moveShapesToPage.test.ts index 75dbb35ce..73d96646d 100644 --- a/packages/tldraw/src/test/commands/moveShapesToPage.test.ts +++ b/packages/tldraw/src/test/commands/moveShapesToPage.test.ts @@ -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 } ) }) diff --git a/packages/tldraw/src/test/commands/reparentShapesById.test.ts b/packages/tldraw/src/test/commands/reparentShapesById.test.ts index d7cdadd2b..eaa3756cd 100644 --- a/packages/tldraw/src/test/commands/reparentShapesById.test.ts +++ b/packages/tldraw/src/test/commands/reparentShapesById.test.ts @@ -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 diff --git a/packages/tldraw/src/test/commands/setCurrentPage.test.ts b/packages/tldraw/src/test/commands/setCurrentPage.test.ts index 64f257dc3..3ecd170c7 100644 --- a/packages/tldraw/src/test/commands/setCurrentPage.test.ts +++ b/packages/tldraw/src/test/commands/setCurrentPage.test.ts @@ -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) diff --git a/packages/tldraw/src/test/test-jsx.tsx b/packages/tldraw/src/test/test-jsx.tsx index 1462ed58d..d48217b4b 100644 --- a/packages/tldraw/src/test/test-jsx.tsx +++ b/packages/tldraw/src/test/test-jsx.tsx @@ -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, 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 diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index 8994f7b1f..17f746706 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -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 extends BaseRecord<'ass // @public (undocumented) export interface TLBaseShape 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) diff --git a/packages/tlschema/api/api.json b/packages/tlschema/api/api.json index d6be06e98..2ffd69fda 100644 --- a/packages/tlschema/api/api.json +++ b/packages/tlschema/api/api.json @@ -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", diff --git a/packages/tlschema/src/TLStore.ts b/packages/tlschema/src/TLStore.ts index cd7f64bd5..2c77aec83 100644 --- a/packages/tlschema/src/TLStore.ts +++ b/packages/tlschema/src/TLStore.ts @@ -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: {}, + }), ] } diff --git a/packages/tlschema/src/misc/TLHandle.ts b/packages/tlschema/src/misc/TLHandle.ts index 2fcb04939..1dee8e9d8 100644 --- a/packages/tlschema/src/misc/TLHandle.ts +++ b/packages/tlschema/src/misc/TLHandle.ts @@ -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 = 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, }) diff --git a/packages/tlschema/src/records/TLPage.ts b/packages/tlschema/src/records/TLPage.ts index 457495563..a0c8a51c0 100644 --- a/packages/tlschema/src/records/TLPage.ts +++ b/packages/tlschema/src/records/TLPage.ts @@ -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 = T.model( typeName: T.literal('page'), id: pageIdValidator, name: T.string, - index: T.string, + index: T.indexKey, meta: T.jsonValue as T.ObjectValidator, }) ) diff --git a/packages/tlschema/src/shapes/TLBaseShape.ts b/packages/tlschema/src/shapes/TLBaseShape.ts index ddabf29b8..7853a54c1 100644 --- a/packages/tlschema/src/shapes/TLBaseShape.ts +++ b/packages/tlschema/src/shapes/TLBaseShape.ts @@ -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 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, diff --git a/packages/tlsync/src/lib/TLSyncRoom.ts b/packages/tlsync/src/lib/TLSyncRoom.ts index 88b568d87..f169473b9 100644 --- a/packages/tlsync/src/lib/TLSyncRoom.ts +++ b/packages/tlsync/src/lib/TLSyncRoom.ts @@ -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 { lastChangedClock: 0, }, { - state: PageRecordType.create({ name: 'Page 1', index: 'a1' }), + state: PageRecordType.create({ name: 'Page 1', index: 'a1' as IndexKey }), lastChangedClock: 0, }, ], diff --git a/packages/tlsync/src/test/TLSyncRoom.test.ts b/packages/tlsync/src/test/TLSyncRoom.test.ts index e3f8bcd79..557f7d504 100644 --- a/packages/tlsync/src/test/TLSyncRoom.test.ts +++ b/packages/tlsync/src/test/TLSyncRoom.test.ts @@ -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 = {}) => ({ @@ -37,7 +37,7 @@ const oldArrow: TLBaseShape<'arrow', Omit> = { typeName: 'shape', type: 'arrow', id: 'shape:old_arrow' as TLShapeId, - index: 'a0', + index: ZERO_INDEX_KEY, isLocked: false, parentId: PageRecordType.createId(), rotation: 0, diff --git a/packages/utils/api-report.md b/packages/utils/api-report.md index 5160ede78..b743dc4b4 100644 --- a/packages/utils/api-report.md +++ b/packages/utils/api-report.md @@ -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(obj: Partial>, 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(a: T, b: T): -1 | 1; +// @public +export function sortByIndex(a: T, b: T): -1 | 0 | 1; + // @public (undocumented) const structuredClone_2: (i: T) => T; export { structuredClone_2 as structuredClone } @@ -262,9 +293,15 @@ export function throttle 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) ``` diff --git a/packages/utils/api/api.json b/packages/utils/api/api.json index b0415b6cb..7a5a4b0c0 100644 --- a/packages/utils/api/api.json +++ b/packages/utils/api/api.json @@ -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(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 + } } ] } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index acf168fc0..4993266ea 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -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' diff --git a/packages/utils/src/lib/reordering/IndexKey.ts b/packages/utils/src/lib/reordering/IndexKey.ts new file mode 100644 index 000000000..b5ca759a4 --- /dev/null +++ b/packages/utils/src/lib/reordering/IndexKey.ts @@ -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 } diff --git a/packages/editor/src/lib/utils/reordering/dgreensp/dgreensp.test.ts b/packages/utils/src/lib/reordering/dgreensp/dgreensp.test.ts similarity index 82% rename from packages/editor/src/lib/utils/reordering/dgreensp/dgreensp.test.ts rename to packages/utils/src/lib/reordering/dgreensp/dgreensp.test.ts index 2f898a543..f64a48880 100644 --- a/packages/editor/src/lib/utils/reordering/dgreensp/dgreensp.test.ts +++ b/packages/utils/src/lib/reordering/dgreensp/dgreensp.test.ts @@ -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' ) }) diff --git a/packages/editor/src/lib/utils/reordering/dgreensp/dgreensp.ts b/packages/utils/src/lib/reordering/dgreensp/dgreensp.ts similarity index 90% rename from packages/editor/src/lib/utils/reordering/dgreensp/dgreensp.ts rename to packages/utils/src/lib/reordering/dgreensp/dgreensp.ts index 7e913655c..20739d792 100644 --- a/packages/editor/src/lib/utils/reordering/dgreensp/dgreensp.ts +++ b/packages/utils/src/lib/reordering/dgreensp/dgreensp.ts @@ -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) { diff --git a/packages/editor/src/lib/utils/reordering/reordering.ts b/packages/utils/src/lib/reordering/reordering.ts similarity index 63% rename from packages/editor/src/lib/utils/reordering/reordering.ts rename to packages/utils/src/lib/reordering/reordering.ts index ea523fb92..189dffc12 100644 --- a/packages/editor/src/lib/utils/reordering/reordering.ts +++ b/packages/utils/src/lib/reordering/reordering.ts @@ -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(a: T, b: T) { +export function sortByIndex(a: T, b: T) { if (a.index < b.index) { return -1 } else if (a.index > b.index) { diff --git a/packages/validate/api-report.md b/packages/validate/api-report.md index 5ba54ba22..b44556ed9 100644 --- a/packages/validate/api-report.md +++ b/packages/validate/api-report.md @@ -4,6 +4,7 @@ ```ts +import { IndexKey } from '@tldraw/utils'; import { JsonValue } from '@tldraw/utils'; // @public @@ -44,6 +45,9 @@ export class DictValidator extends Validator; } +// @public +const indexKey: Validator; + // @public const integer: Validator; @@ -155,7 +159,8 @@ declare namespace T { unknownObject, jsonValue, linkUrl, - srcUrl + srcUrl, + indexKey } } export { T } diff --git a/packages/validate/api/api.json b/packages/validate/api/api.json index 37b598734..1920a5626 100644 --- a/packages/validate/api/api.json +++ b/packages/validate/api/api.json @@ -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", diff --git a/packages/validate/src/lib/validation.ts b/packages/validate/src/lib/validation.ts index 77fa46637..0a34cb08f 100644 --- a/packages/validate/src/lib/validation.ts +++ b/packages/validate/src/lib/validation.ts @@ -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 = (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((key) => { + try { + validateIndexKey(key) + return key + } catch { + throw new ValidationError(`Expected an index key, got ${JSON.stringify(key)}`) + } +}) diff --git a/packages/validate/src/test/validation.test.ts b/packages/validate/src/test/validation.test.ts index e74dc69cf..b8b363876 100644 --- a/packages/validate/src/test/validation.test.ts +++ b/packages/validate/src/test/validation.test.ts @@ -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 """` + ) + }) +})