Files
upstream-tldraw/templates/branching-chat/client/connection/keepConnectionsAtBottom.tsx
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

117 lines
4.3 KiB
TypeScript

import { Editor, getIndexBetween, getIndicesBetween, TLParentId } from 'tldraw'
/**
* This function registers side effects that will make sure connection shapes are always below all
* other shapes in the same parent. We do this because we don't want to connections on top of nodes,
* which might be distracting.
*/
export function keepConnectionsAtBottom(editor: Editor) {
// whenever an operation happens, we'll keep track of the parent ids that we might need to
// re-sort the children of. An operation is any atomic change to the tldraw store.
let pendingChangedParentIds = new Set<TLParentId>()
// whenever a shape is created, we'll add the parent id to the set of parent ids that we might
// need to re-sort the children of.
editor.sideEffects.registerAfterCreateHandler('shape', (shape, source) => {
if (source === 'remote') return
pendingChangedParentIds.add(shape.parentId)
})
// whenever a shape's index or parent id changes, we'll add the parent id to the set of parent
// ids that we might need to re-sort the children of.
editor.sideEffects.registerAfterChangeHandler('shape', (oldShape, newShape, source) => {
if (source === 'remote') return
if (oldShape.parentId === newShape.parentId && oldShape.index === newShape.index) return
pendingChangedParentIds.add(newShape.parentId)
})
// then, when the operation is complete, we re-sort the children as needed:
editor.sideEffects.registerOperationCompleteHandler(() => {
if (pendingChangedParentIds.size === 0) return
// reset the set of pending changed parent ids
const changedParentIds = pendingChangedParentIds
pendingChangedParentIds = new Set()
const updates = []
for (const parentId of changedParentIds) {
// iterate through all the children of this parent. We want all the connections at the
// bottom (first), and everything else after that. Because this fn runs all the time,
// things should always be roughly in the right place to begin with. Because of that,
// we'll just find any non-connections that are in the wrong place and move them up.
const childIds = editor.getSortedChildIdsForParent(parentId)
// we iterate in reverse because we want to find the highest connection index:
let i = childIds.length - 1
let highestConnectionIndex = null
let nextIndexAboveHighestConnectionIndex = null
for (; i >= 0; i--) {
const child = editor.getShape(childIds[i])
if (!child) continue
if (child.type === 'connection') {
highestConnectionIndex = child.index
break
} else {
nextIndexAboveHighestConnectionIndex = child.index
}
}
// now that we've found the highest connection index, we switch to looking for
// non-connection shapes. these need to be moved up above the highest connection index.
const shapesToMove = []
for (; i >= 0; i--) {
const child = editor.getShape(childIds[i])
if (child && child.type !== 'connection') shapesToMove.push(child)
}
// the shapesToMove array will be in reverse order, so we need to reverse it:
shapesToMove.reverse()
// now we need the new indexes for each shape. These need to stay in the same relative
// order, and be inserted above the `hightestConnectionIndex`, but below the index of
// the next non-connection shape.
const newIndexes = getIndicesBetween(
highestConnectionIndex,
nextIndexAboveHighestConnectionIndex,
shapesToMove.length
)
// now we can create the update partials for those shapes:
for (let i = 0; i < shapesToMove.length; i++) {
const shape = shapesToMove[i]
updates.push({ id: shape.id, type: shape.type, index: newIndexes[i] } as const)
}
}
editor.updateShapes(updates)
})
}
/**
* Get the index of the next connection shape in the given parent. This will be above all other
* connections, but the connections should be below all other shapes.
*/
export function getNextConnectionIndex(
editor: Editor,
parentId: TLParentId = editor.getCurrentPageId()
) {
const childIds = editor.getSortedChildIdsForParent(parentId)
let prevIndex = null
let highestConnectionIndex = null
for (let i = childIds.length - 1; i >= 0; i--) {
const child = editor.getShape(childIds[i])
if (!child) continue
if (child.type === 'connection') {
highestConnectionIndex = child.index
break
}
prevIndex = child.index
}
return getIndexBetween(highestConnectionIndex, prevIndex)
}