usePaste.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import {
  2. BoundsUtils,
  3. fileToBase64,
  4. getSizeFromSrc,
  5. TLAsset,
  6. TLBinding,
  7. TLShapeModel,
  8. uniqueId,
  9. } from '@tldraw/core'
  10. import type { TLReactCallbacks } from '@tldraw/react'
  11. import * as React from 'react'
  12. import type { Shape } from '~lib'
  13. export function usePaste() {
  14. return React.useCallback<TLReactCallbacks<Shape>['onFileDrop']>(async (app, { point }) => {
  15. const assetId = uniqueId()
  16. interface ImageAsset extends TLAsset {
  17. size: number[]
  18. }
  19. const assetsToCreate: ImageAsset[] = []
  20. const shapesToCreate: TLShapeModel[] = []
  21. const bindingsToCreate: TLBinding[] = []
  22. async function handleImage(item: ClipboardItem) {
  23. const firstImageType = item.types.find(type => type.startsWith('image'))
  24. if (firstImageType) {
  25. const blob = await item.getType(firstImageType)
  26. const dataurl = await fileToBase64(blob)
  27. if (typeof dataurl !== 'string') return false
  28. const existingAsset = Object.values(app.assets).find(asset => asset.src === dataurl)
  29. if (existingAsset) {
  30. assetsToCreate.push(existingAsset as ImageAsset)
  31. return false
  32. }
  33. // Create a new asset for this image
  34. const asset: ImageAsset = {
  35. id: assetId,
  36. type: 'image',
  37. src: dataurl,
  38. size: await getSizeFromSrc(dataurl),
  39. }
  40. assetsToCreate.push(asset)
  41. return true
  42. }
  43. return false
  44. }
  45. async function handleLogseqShapes(item: ClipboardItem) {
  46. const plainTextType = item.types.find(type => type.startsWith('text/plain'))
  47. if (plainTextType) {
  48. const blob = await item.getType(plainTextType)
  49. const rawText = await blob.text()
  50. const data = JSON.parse(rawText)
  51. if (data.type === 'logseq/whiteboard-shapes') {
  52. const shapes = data.shapes as TLShapeModel[]
  53. const commonBounds = BoundsUtils.getCommonBounds(
  54. shapes.map(shape => ({
  55. minX: shape.point?.[0] ?? point[0],
  56. minY: shape.point?.[1] ?? point[1],
  57. width: shape.size?.[0] ?? 4,
  58. height: shape.size?.[1] ?? 4,
  59. maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
  60. maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
  61. }))
  62. )
  63. const clonedShapes = shapes.map((shape: TLShapeModel) => {
  64. return {
  65. ...shape,
  66. id: uniqueId(),
  67. parentId: app.currentPageId,
  68. point: [
  69. point[0] + shape.point![0] - commonBounds.minX,
  70. point[1] + shape.point![1] - commonBounds.minY,
  71. ],
  72. }
  73. })
  74. shapesToCreate.push(...clonedShapes)
  75. // Try to rebinding the shapes to the new assets
  76. shapesToCreate.forEach((s, idx) => {
  77. if (s.handles) {
  78. Object.values(s.handles).forEach(h => {
  79. if (h.bindingId) {
  80. // try to bind the new shape
  81. const binding = app.currentPage.bindings[h.bindingId]
  82. // if the copied binding from/to is in the source
  83. const oldFromIdx = shapes.findIndex(s => s.id === binding.fromId)
  84. const oldToIdx = shapes.findIndex(s => s.id === binding.toId)
  85. if (binding && oldFromIdx !== -1 && oldToIdx !== -1) {
  86. const newBinding: TLBinding = {
  87. ...binding,
  88. id: uniqueId(),
  89. fromId: shapesToCreate[oldFromIdx].id,
  90. toId: shapesToCreate[oldToIdx].id,
  91. }
  92. bindingsToCreate.push(newBinding)
  93. h.bindingId = newBinding.id
  94. } else {
  95. h.bindingId = undefined
  96. }
  97. }
  98. })
  99. }
  100. })
  101. }
  102. }
  103. }
  104. // TODO: supporting other pasting formats
  105. for (const item of await navigator.clipboard.read()) {
  106. try {
  107. let handled = await handleImage(item)
  108. if (!handled) {
  109. await handleLogseqShapes(item)
  110. }
  111. } catch (error) {
  112. console.error(error)
  113. }
  114. }
  115. const allShapesToAdd: TLShapeModel[] = [
  116. ...assetsToCreate.map((asset, i) => ({
  117. id: uniqueId(),
  118. type: 'image',
  119. parentId: app.currentPageId,
  120. point: [point[0] - asset.size[0] / 2 + i * 16, point[1] - asset.size[1] / 2 + i * 16],
  121. size: asset.size,
  122. assetId: asset.id,
  123. opacity: 1,
  124. })),
  125. ...shapesToCreate,
  126. ]
  127. app.createAssets(assetsToCreate)
  128. app.createShapes(allShapesToAdd)
  129. app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
  130. app.setSelectedShapes(allShapesToAdd.map(s => s.id))
  131. }, [])
  132. }