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
117 lines
4.3 KiB
TypeScript
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)
|
|
}
|