|
|
@@ -4,11 +4,12 @@ import {
|
|
|
TLAsset,
|
|
|
TLBinding,
|
|
|
TLCursor,
|
|
|
+ TLPasteEventInfo,
|
|
|
TLShapeModel,
|
|
|
uniqueId,
|
|
|
validUUID,
|
|
|
} from '@tldraw/core'
|
|
|
-import type { TLReactCallbacks } from '@tldraw/react'
|
|
|
+import type { TLReactApp, TLReactCallbacks } from '@tldraw/react'
|
|
|
import Vec from '@tldraw/vec'
|
|
|
import * as React from 'react'
|
|
|
import { NIL as NIL_UUID } from 'uuid'
|
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
YouTubeShape,
|
|
|
type Shape,
|
|
|
} from '../lib'
|
|
|
-import { LogseqContext } from '../lib/logseq-context'
|
|
|
+import { LogseqContext, LogseqContextValue } from '../lib/logseq-context'
|
|
|
|
|
|
const isValidURL = (url: string) => {
|
|
|
try {
|
|
|
@@ -87,323 +88,358 @@ async function getDataFromType(item: DataTransfer | ClipboardItem, type: `text/$
|
|
|
return await blob.text()
|
|
|
}
|
|
|
|
|
|
-// FIXME: for assets, we should prompt the user a loading spinner
|
|
|
-export function usePaste() {
|
|
|
- const { handlers } = React.useContext(LogseqContext)
|
|
|
-
|
|
|
- return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(
|
|
|
- async (app, { point, shiftKey, dataTransfer, fromDrop }) => {
|
|
|
- let imageAssetsToCreate: VideoImageAsset[] = []
|
|
|
- let assetsToClone: TLAsset[] = []
|
|
|
- const bindingsToCreate: TLBinding[] = []
|
|
|
-
|
|
|
- async function createAssetsFromURL(url: string, isVideo: boolean): Promise<VideoImageAsset> {
|
|
|
- // Do we already have an asset for this image?
|
|
|
- const existingAsset = Object.values(app.assets).find(asset => asset.src === url)
|
|
|
- if (existingAsset) {
|
|
|
- return existingAsset as VideoImageAsset
|
|
|
- } else {
|
|
|
- // Create a new asset for this image
|
|
|
- const asset: VideoImageAsset = {
|
|
|
- id: uniqueId(),
|
|
|
- type: isVideo ? 'video' : 'image',
|
|
|
- src: url,
|
|
|
- size: await getSizeFromSrc(handlers.makeAssetUrl(url), isVideo),
|
|
|
- }
|
|
|
- return asset
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async function createAssetsFromFiles(files: File[]) {
|
|
|
- const tasks = files
|
|
|
- .filter(file => getFileType(file.name) !== 'unknown')
|
|
|
- .map(async file => {
|
|
|
- try {
|
|
|
- const dataurl = await handlers.saveAsset(file)
|
|
|
- return await createAssetsFromURL(dataurl, getFileType(file.name) === 'video')
|
|
|
- } catch (err) {
|
|
|
- console.error(err)
|
|
|
- }
|
|
|
- return null
|
|
|
- })
|
|
|
- return (await Promise.all(tasks)).filter(isNonNullable)
|
|
|
+const handleCreatingShapes = async (
|
|
|
+ app: TLReactApp<Shape>,
|
|
|
+ { point, shiftKey, dataTransfer, fromDrop }: TLPasteEventInfo,
|
|
|
+ handlers: LogseqContextValue['handlers']
|
|
|
+) => {
|
|
|
+ let imageAssetsToCreate: VideoImageAsset[] = []
|
|
|
+ let assetsToClone: TLAsset[] = []
|
|
|
+ const bindingsToCreate: TLBinding[] = []
|
|
|
+
|
|
|
+ async function createAssetsFromURL(url: string, isVideo: boolean): Promise<VideoImageAsset> {
|
|
|
+ // Do we already have an asset for this image?
|
|
|
+ const existingAsset = Object.values(app.assets).find(asset => asset.src === url)
|
|
|
+ if (existingAsset) {
|
|
|
+ return existingAsset as VideoImageAsset
|
|
|
+ } else {
|
|
|
+ // Create a new asset for this image
|
|
|
+ const asset: VideoImageAsset = {
|
|
|
+ id: uniqueId(),
|
|
|
+ type: isVideo ? 'video' : 'image',
|
|
|
+ src: url,
|
|
|
+ size: await getSizeFromSrc(handlers.makeAssetUrl(url), isVideo),
|
|
|
}
|
|
|
+ return asset
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- function createHTMLShape(text: string) {
|
|
|
- return [
|
|
|
- {
|
|
|
- ...HTMLShape.defaultProps,
|
|
|
- html: text,
|
|
|
- point: [point[0], point[1]],
|
|
|
- },
|
|
|
- ]
|
|
|
- }
|
|
|
+ async function createAssetsFromFiles(files: File[]) {
|
|
|
+ const tasks = files
|
|
|
+ .filter(file => getFileType(file.name) !== 'unknown')
|
|
|
+ .map(async file => {
|
|
|
+ try {
|
|
|
+ const dataurl = await handlers.saveAsset(file)
|
|
|
+ return await createAssetsFromURL(dataurl, getFileType(file.name) === 'video')
|
|
|
+ } catch (err) {
|
|
|
+ console.error(err)
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ })
|
|
|
+ return (await Promise.all(tasks)).filter(isNonNullable)
|
|
|
+ }
|
|
|
|
|
|
- async function tryCreateShapesFromDataTransfer(dataTransfer: DataTransfer) {
|
|
|
- return tryCreateShapeHelper(
|
|
|
- tryCreateShapeFromFiles,
|
|
|
- tryCreateShapeFromTextHTML,
|
|
|
- tryCreateShapeFromTextPlain,
|
|
|
- tryCreateShapeFromBlockUUID
|
|
|
- )(dataTransfer)
|
|
|
- }
|
|
|
+ function createHTMLShape(text: string) {
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ ...HTMLShape.defaultProps,
|
|
|
+ html: text,
|
|
|
+ point: [point[0], point[1]],
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ }
|
|
|
|
|
|
- async function tryCreateShapesFromClipboard() {
|
|
|
- const items = await navigator.clipboard.read()
|
|
|
- const createShapesFn = tryCreateShapeHelper(
|
|
|
- tryCreateShapeFromTextHTML,
|
|
|
- tryCreateShapeFromTextPlain
|
|
|
- )
|
|
|
- const allShapes = (await Promise.all(items.map(item => createShapesFn(item))))
|
|
|
- .flat()
|
|
|
- .filter(isNonNullable)
|
|
|
-
|
|
|
- return allShapes
|
|
|
- }
|
|
|
+ async function tryCreateShapesFromDataTransfer(dataTransfer: DataTransfer) {
|
|
|
+ return tryCreateShapeHelper(
|
|
|
+ tryCreateShapeFromFiles,
|
|
|
+ tryCreateShapeFromTextHTML,
|
|
|
+ tryCreateShapeFromTextPlain,
|
|
|
+ tryCreateShapeFromBlockUUID
|
|
|
+ )(dataTransfer)
|
|
|
+ }
|
|
|
|
|
|
- async function tryCreateShapeFromFiles(item: DataTransfer) {
|
|
|
- const files = Array.from(item.files)
|
|
|
- if (files.length > 0) {
|
|
|
- const assets = await createAssetsFromFiles(files)
|
|
|
- // ? could we get rid of this side effect?
|
|
|
- imageAssetsToCreate = assets
|
|
|
-
|
|
|
- return assets.map((asset, i) => {
|
|
|
- const defaultProps =
|
|
|
- asset.type === 'video' ? VideoShape.defaultProps : ImageShape.defaultProps
|
|
|
- const newShape = {
|
|
|
- ...defaultProps,
|
|
|
- id: uniqueId(),
|
|
|
- // TODO: Should be place near the last edited shape
|
|
|
- assetId: asset.id,
|
|
|
- opacity: 1,
|
|
|
- }
|
|
|
+ async function tryCreateShapesFromClipboard() {
|
|
|
+ const items = await navigator.clipboard.read()
|
|
|
+ const createShapesFn = tryCreateShapeHelper(
|
|
|
+ tryCreateShapeFromTextHTML,
|
|
|
+ tryCreateShapeFromTextPlain
|
|
|
+ )
|
|
|
+ const allShapes = (await Promise.all(items.map(item => createShapesFn(item))))
|
|
|
+ .flat()
|
|
|
+ .filter(isNonNullable)
|
|
|
+
|
|
|
+ return allShapes
|
|
|
+ }
|
|
|
|
|
|
- if (asset.size) {
|
|
|
- Object.assign(newShape, {
|
|
|
- point: [
|
|
|
- point[0] - asset.size[0] / 4 + i * 16,
|
|
|
- point[1] - asset.size[1] / 4 + i * 16,
|
|
|
- ],
|
|
|
- size: Vec.div(asset.size, 2),
|
|
|
- })
|
|
|
- }
|
|
|
+ async function tryCreateShapeFromFiles(item: DataTransfer) {
|
|
|
+ const files = Array.from(item.files)
|
|
|
+ if (files.length > 0) {
|
|
|
+ const assets = await createAssetsFromFiles(files)
|
|
|
+ // ? could we get rid of this side effect?
|
|
|
+ imageAssetsToCreate = assets
|
|
|
+
|
|
|
+ return assets.map((asset, i) => {
|
|
|
+ const defaultProps =
|
|
|
+ asset.type === 'video' ? VideoShape.defaultProps : ImageShape.defaultProps
|
|
|
+ const newShape = {
|
|
|
+ ...defaultProps,
|
|
|
+ id: uniqueId(),
|
|
|
+ // TODO: Should be place near the last edited shape
|
|
|
+ assetId: asset.id,
|
|
|
+ opacity: 1,
|
|
|
+ }
|
|
|
|
|
|
- return newShape
|
|
|
+ if (asset.size) {
|
|
|
+ Object.assign(newShape, {
|
|
|
+ point: [point[0] - asset.size[0] / 4 + i * 16, point[1] - asset.size[1] / 4 + i * 16],
|
|
|
+ size: Vec.div(asset.size, 2),
|
|
|
})
|
|
|
}
|
|
|
- return null
|
|
|
- }
|
|
|
|
|
|
- async function tryCreateShapeFromTextHTML(item: DataTransfer | ClipboardItem) {
|
|
|
- // skips if it's a drop event or using shift key
|
|
|
- if (item.types.includes('text/plain') && (shiftKey || fromDrop)) {
|
|
|
- return null
|
|
|
- }
|
|
|
- const rawText = await getDataFromType(item, 'text/html')
|
|
|
- if (rawText) {
|
|
|
- return tryCreateShapeHelper(tryCreateClonedShapesFromJSON, createHTMLShape)(rawText)
|
|
|
- }
|
|
|
- return null
|
|
|
- }
|
|
|
+ return newShape
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ }
|
|
|
|
|
|
- async function tryCreateShapeFromBlockUUID(dataTransfer: DataTransfer) {
|
|
|
- // This is a Logseq custom data type defined in frontend.components.block
|
|
|
- const rawText = dataTransfer.getData('block-uuid')
|
|
|
- if (rawText) {
|
|
|
- const text = rawText.trim()
|
|
|
- const allSelectedBlocks = window.logseq?.api?.get_selected_blocks?.()
|
|
|
- const blockUUIDs =
|
|
|
- allSelectedBlocks && allSelectedBlocks?.length > 1
|
|
|
- ? allSelectedBlocks.map(b => b.uuid)
|
|
|
- : [text]
|
|
|
- // ensure all uuid in blockUUIDs is persisted
|
|
|
- window.logseq?.api?.set_blocks_id?.(blockUUIDs)
|
|
|
- const tasks = blockUUIDs.map(uuid => tryCreateLogseqPortalShapesFromString(`((${uuid}))`))
|
|
|
- const newShapes = (await Promise.all(tasks)).flat().filter(isNonNullable)
|
|
|
- return newShapes.map((s, idx) => {
|
|
|
- // if there are multiple shapes, shift them to the right
|
|
|
- return {
|
|
|
- ...s,
|
|
|
- // TODO: use better alignment?
|
|
|
- point: [point[0] + (LogseqPortalShape.defaultProps.size[0] + 16) * idx, point[1]],
|
|
|
- }
|
|
|
- })
|
|
|
- }
|
|
|
- return null
|
|
|
- }
|
|
|
+ async function tryCreateShapeFromTextHTML(item: DataTransfer | ClipboardItem) {
|
|
|
+ // skips if it's a drop event or using shift key
|
|
|
+ if (item.types.includes('text/plain') && (shiftKey || fromDrop)) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ const rawText = await getDataFromType(item, 'text/html')
|
|
|
+ if (rawText) {
|
|
|
+ return tryCreateShapeHelper(tryCreateClonedShapesFromJSON, createHTMLShape)(rawText)
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ }
|
|
|
|
|
|
- async function tryCreateShapeFromTextPlain(item: DataTransfer | ClipboardItem) {
|
|
|
- const rawText = await getDataFromType(item, 'text/plain')
|
|
|
- if (rawText) {
|
|
|
- const text = rawText.trim()
|
|
|
- return tryCreateShapeHelper(
|
|
|
- tryCreateShapeFromURL,
|
|
|
- tryCreateShapeFromIframeString,
|
|
|
- tryCreateLogseqPortalShapesFromString
|
|
|
- )(text)
|
|
|
+ async function tryCreateShapeFromBlockUUID(dataTransfer: DataTransfer) {
|
|
|
+ // This is a Logseq custom data type defined in frontend.components.block
|
|
|
+ const rawText = dataTransfer.getData('block-uuid')
|
|
|
+ if (rawText) {
|
|
|
+ const text = rawText.trim()
|
|
|
+ const allSelectedBlocks = window.logseq?.api?.get_selected_blocks?.()
|
|
|
+ const blockUUIDs =
|
|
|
+ allSelectedBlocks && allSelectedBlocks?.length > 1
|
|
|
+ ? allSelectedBlocks.map(b => b.uuid)
|
|
|
+ : [text]
|
|
|
+ // ensure all uuid in blockUUIDs is persisted
|
|
|
+ window.logseq?.api?.set_blocks_id?.(blockUUIDs)
|
|
|
+ const tasks = blockUUIDs.map(uuid => tryCreateLogseqPortalShapesFromString(`((${uuid}))`))
|
|
|
+ const newShapes = (await Promise.all(tasks)).flat().filter(isNonNullable)
|
|
|
+ return newShapes.map((s, idx) => {
|
|
|
+ // if there are multiple shapes, shift them to the right
|
|
|
+ return {
|
|
|
+ ...s,
|
|
|
+ // TODO: use better alignment?
|
|
|
+ point: [point[0] + (LogseqPortalShape.defaultProps.size[0] + 16) * idx, point[1]],
|
|
|
}
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ }
|
|
|
|
|
|
- return null
|
|
|
- }
|
|
|
+ async function tryCreateShapeFromTextPlain(item: DataTransfer | ClipboardItem) {
|
|
|
+ const rawText = await getDataFromType(item, 'text/plain')
|
|
|
+ if (rawText) {
|
|
|
+ const text = rawText.trim()
|
|
|
+ return tryCreateShapeHelper(
|
|
|
+ tryCreateShapeFromURL,
|
|
|
+ tryCreateShapeFromIframeString,
|
|
|
+ tryCreateLogseqPortalShapesFromString
|
|
|
+ )(text)
|
|
|
+ }
|
|
|
|
|
|
- function tryCreateClonedShapesFromJSON(rawText: string) {
|
|
|
- const result = app.api.getClonedShapesFromTldrString(decodeURIComponent(rawText), point)
|
|
|
- if (result) {
|
|
|
- const { shapes, assets, bindings } = result
|
|
|
- assetsToClone.push(...assets)
|
|
|
- bindingsToCreate.push(...bindings)
|
|
|
- return shapes
|
|
|
- }
|
|
|
- return null
|
|
|
- }
|
|
|
+ return null
|
|
|
+ }
|
|
|
|
|
|
- async function tryCreateShapeFromURL(rawText: string) {
|
|
|
- if (isValidURL(rawText) && !(shiftKey || fromDrop)) {
|
|
|
- const isYoutubeUrl = (url: string) => {
|
|
|
- const youtubeRegex =
|
|
|
- /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
|
|
|
- return youtubeRegex.test(url)
|
|
|
- }
|
|
|
- if (isYoutubeUrl(rawText)) {
|
|
|
- return [
|
|
|
- {
|
|
|
- ...YouTubeShape.defaultProps,
|
|
|
- url: rawText,
|
|
|
- point: [point[0], point[1]],
|
|
|
- },
|
|
|
- ]
|
|
|
- }
|
|
|
+ function tryCreateClonedShapesFromJSON(rawText: string) {
|
|
|
+ const result = app.api.getClonedShapesFromTldrString(decodeURIComponent(rawText), point)
|
|
|
+ if (result) {
|
|
|
+ const { shapes, assets, bindings } = result
|
|
|
+ assetsToClone.push(...assets)
|
|
|
+ bindingsToCreate.push(...bindings)
|
|
|
+ return shapes
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ }
|
|
|
|
|
|
- return [
|
|
|
- {
|
|
|
- ...IFrameShape.defaultProps,
|
|
|
- url: rawText,
|
|
|
- point: [point[0], point[1]],
|
|
|
- },
|
|
|
- ]
|
|
|
- }
|
|
|
- return null
|
|
|
+ async function tryCreateShapeFromURL(rawText: string) {
|
|
|
+ if (isValidURL(rawText) && !(shiftKey || fromDrop)) {
|
|
|
+ const isYoutubeUrl = (url: string) => {
|
|
|
+ const youtubeRegex =
|
|
|
+ /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
|
|
|
+ return youtubeRegex.test(url)
|
|
|
}
|
|
|
-
|
|
|
- function tryCreateShapeFromIframeString(rawText: string) {
|
|
|
- // if rawText is iframe text
|
|
|
- if (rawText.startsWith('<iframe')) {
|
|
|
- return [
|
|
|
- {
|
|
|
- ...HTMLShape.defaultProps,
|
|
|
- html: rawText,
|
|
|
- point: [point[0], point[1]],
|
|
|
- },
|
|
|
- ]
|
|
|
- }
|
|
|
- return null
|
|
|
+ if (isYoutubeUrl(rawText)) {
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ ...YouTubeShape.defaultProps,
|
|
|
+ url: rawText,
|
|
|
+ point: [point[0], point[1]],
|
|
|
+ },
|
|
|
+ ]
|
|
|
}
|
|
|
|
|
|
- async function tryCreateLogseqPortalShapesFromString(rawText: string) {
|
|
|
- if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
|
|
|
- const blockRef = rawText.slice(2, -2)
|
|
|
- if (validUUID(blockRef)) {
|
|
|
- return [
|
|
|
- {
|
|
|
- ...LogseqPortalShape.defaultProps,
|
|
|
- point: [point[0], point[1]],
|
|
|
- size: [400, 0], // use 0 here to enable auto-resize
|
|
|
- pageId: blockRef,
|
|
|
- fill: app.settings.color,
|
|
|
- stroke: app.settings.color,
|
|
|
- blockType: 'B' as 'B',
|
|
|
- },
|
|
|
- ]
|
|
|
- }
|
|
|
- }
|
|
|
- // [[page name]] ?
|
|
|
- else if (/^\[\[.*\]\]$/.test(rawText)) {
|
|
|
- const pageName = rawText.slice(2, -2)
|
|
|
- return [
|
|
|
- {
|
|
|
- ...LogseqPortalShape.defaultProps,
|
|
|
- point: [point[0], point[1]],
|
|
|
- size: [400, 0], // use 0 here to enable auto-resize
|
|
|
- pageId: pageName,
|
|
|
- fill: app.settings.color,
|
|
|
- stroke: app.settings.color,
|
|
|
- blockType: 'P' as 'P',
|
|
|
- },
|
|
|
- ]
|
|
|
- }
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ ...IFrameShape.defaultProps,
|
|
|
+ url: rawText,
|
|
|
+ point: [point[0], point[1]],
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ }
|
|
|
|
|
|
- // Otherwise, creating a new block that belongs to the current whiteboard
|
|
|
- const uuid = handlers?.addNewBlock(rawText)
|
|
|
- if (uuid) {
|
|
|
- // create text shape
|
|
|
- return [
|
|
|
- {
|
|
|
- ...LogseqPortalShape.defaultProps,
|
|
|
- size: [400, 0], // use 0 here to enable auto-resize
|
|
|
- point: [point[0], point[1]],
|
|
|
- pageId: uuid,
|
|
|
- fill: app.settings.color,
|
|
|
- stroke: app.settings.color,
|
|
|
- blockType: 'B' as 'B',
|
|
|
- compact: true,
|
|
|
- },
|
|
|
- ]
|
|
|
- }
|
|
|
+ function tryCreateShapeFromIframeString(rawText: string) {
|
|
|
+ // if rawText is iframe text
|
|
|
+ if (rawText.startsWith('<iframe')) {
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ ...HTMLShape.defaultProps,
|
|
|
+ html: rawText,
|
|
|
+ point: [point[0], point[1]],
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ }
|
|
|
|
|
|
- return null
|
|
|
+ async function tryCreateLogseqPortalShapesFromString(rawText: string) {
|
|
|
+ if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
|
|
|
+ const blockRef = rawText.slice(2, -2)
|
|
|
+ if (validUUID(blockRef)) {
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ ...LogseqPortalShape.defaultProps,
|
|
|
+ point: [point[0], point[1]],
|
|
|
+ size: [400, 0], // use 0 here to enable auto-resize
|
|
|
+ pageId: blockRef,
|
|
|
+ fill: app.settings.color,
|
|
|
+ stroke: app.settings.color,
|
|
|
+ blockType: 'B' as 'B',
|
|
|
+ },
|
|
|
+ ]
|
|
|
}
|
|
|
+ }
|
|
|
+ // [[page name]] ?
|
|
|
+ else if (/^\[\[.*\]\]$/.test(rawText)) {
|
|
|
+ const pageName = rawText.slice(2, -2)
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ ...LogseqPortalShape.defaultProps,
|
|
|
+ point: [point[0], point[1]],
|
|
|
+ size: [400, 0], // use 0 here to enable auto-resize
|
|
|
+ pageId: pageName,
|
|
|
+ fill: app.settings.color,
|
|
|
+ stroke: app.settings.color,
|
|
|
+ blockType: 'P' as 'P',
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ }
|
|
|
|
|
|
- app.cursors.setCursor(TLCursor.Progress)
|
|
|
+ // Otherwise, creating a new block that belongs to the current whiteboard
|
|
|
+ const uuid = handlers?.addNewBlock(rawText)
|
|
|
+ if (uuid) {
|
|
|
+ // create text shape
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ ...LogseqPortalShape.defaultProps,
|
|
|
+ size: [400, 0], // use 0 here to enable auto-resize
|
|
|
+ point: [point[0], point[1]],
|
|
|
+ pageId: uuid,
|
|
|
+ fill: app.settings.color,
|
|
|
+ stroke: app.settings.color,
|
|
|
+ blockType: 'B' as 'B',
|
|
|
+ compact: true,
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ }
|
|
|
|
|
|
- let newShapes: TLShapeModel[] = []
|
|
|
- try {
|
|
|
- if (dataTransfer) {
|
|
|
- newShapes.push(...((await tryCreateShapesFromDataTransfer(dataTransfer)) ?? []))
|
|
|
- } else {
|
|
|
- // from Clipboard app or Shift copy etc
|
|
|
- // in this case, we do not have the dataTransfer object
|
|
|
- newShapes.push(...((await tryCreateShapesFromClipboard()) ?? []))
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error(error)
|
|
|
- }
|
|
|
+ return null
|
|
|
+ }
|
|
|
|
|
|
- const allShapesToAdd: TLShapeModel[] = newShapes.map(shape => {
|
|
|
- return {
|
|
|
- ...shape,
|
|
|
- parentId: app.currentPageId,
|
|
|
- id: validUUID(shape.id) ? shape.id : uniqueId(),
|
|
|
- }
|
|
|
- })
|
|
|
+ app.cursors.setCursor(TLCursor.Progress)
|
|
|
|
|
|
- const filesOnly = dataTransfer?.types.every(t => t === 'Files')
|
|
|
+ let newShapes: TLShapeModel[] = []
|
|
|
+ try {
|
|
|
+ if (dataTransfer) {
|
|
|
+ newShapes.push(...((await tryCreateShapesFromDataTransfer(dataTransfer)) ?? []))
|
|
|
+ } else {
|
|
|
+ // from Clipboard app or Shift copy etc
|
|
|
+ // in this case, we do not have the dataTransfer object
|
|
|
+ newShapes.push(...((await tryCreateShapesFromClipboard()) ?? []))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error)
|
|
|
+ }
|
|
|
|
|
|
- app.wrapUpdate(() => {
|
|
|
- const allAssets = [...imageAssetsToCreate, ...assetsToClone]
|
|
|
- if (allAssets.length > 0) {
|
|
|
- app.createAssets(allAssets)
|
|
|
- }
|
|
|
- if (allShapesToAdd.length > 0) {
|
|
|
- app.createShapes(allShapesToAdd)
|
|
|
- }
|
|
|
- app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
|
|
|
+ const allShapesToAdd: TLShapeModel<Shape['props']>[] = newShapes.map(shape => {
|
|
|
+ return {
|
|
|
+ ...shape,
|
|
|
+ parentId: app.currentPageId,
|
|
|
+ id: validUUID(shape.id) ? shape.id : uniqueId(),
|
|
|
+ }
|
|
|
+ })
|
|
|
|
|
|
- if (app.selectedShapesArray.length === 1 && allShapesToAdd.length === 1 && !fromDrop) {
|
|
|
- const source = app.selectedShapesArray[0]
|
|
|
- const target = app.getShapeById(allShapesToAdd[0].id!)!
|
|
|
- app.createNewLineBinding(source, target)
|
|
|
- }
|
|
|
+ const filesOnly = dataTransfer?.types.every(t => t === 'Files')
|
|
|
+
|
|
|
+ app.wrapUpdate(() => {
|
|
|
+ const allAssets = [...imageAssetsToCreate, ...assetsToClone]
|
|
|
+ if (allAssets.length > 0) {
|
|
|
+ app.createAssets(allAssets)
|
|
|
+ }
|
|
|
+ if (allShapesToAdd.length > 0) {
|
|
|
+ app.createShapes(allShapesToAdd)
|
|
|
+ }
|
|
|
+ app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
|
|
|
+
|
|
|
+ if (app.selectedShapesArray.length === 1 && allShapesToAdd.length === 1 && !fromDrop) {
|
|
|
+ const source = app.selectedShapesArray[0]
|
|
|
+ const target = app.getShapeById(allShapesToAdd[0].id!)!
|
|
|
+ app.createNewLineBinding(source, target)
|
|
|
+ }
|
|
|
|
|
|
- app.setSelectedShapes(allShapesToAdd.map(s => s.id))
|
|
|
- app.selectedTool.transition('idle') // clears possible editing states
|
|
|
- app.cursors.setCursor(TLCursor.Default)
|
|
|
+ app.setSelectedShapes(allShapesToAdd.map(s => s.id))
|
|
|
+ app.selectedTool.transition('idle') // clears possible editing states
|
|
|
+ app.cursors.setCursor(TLCursor.Default)
|
|
|
|
|
|
- if (fromDrop || filesOnly) {
|
|
|
- app.packIntoRectangle()
|
|
|
+ if (fromDrop || filesOnly) {
|
|
|
+ app.packIntoRectangle()
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// FIXME: for assets, we should prompt the user a loading spinner
|
|
|
+export function usePaste() {
|
|
|
+ const { handlers } = React.useContext(LogseqContext)
|
|
|
+
|
|
|
+ return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(async (app, info) => {
|
|
|
+ // there is a special case for SHIFT+PASTE
|
|
|
+ // it will set the link to the current selected shape
|
|
|
+
|
|
|
+ if (info.shiftKey && app.selectedShapesArray.length === 1) {
|
|
|
+ // TODO: thinking about how to make this more generic with usePaste hook
|
|
|
+ // TODO: handle whiteboard shapes?
|
|
|
+ const items = await navigator.clipboard.read()
|
|
|
+ let newRef: string | undefined
|
|
|
+ if (items.length > 0) {
|
|
|
+ const blob = await items[0].getType('text/plain')
|
|
|
+ const rawText = (await blob.text()).trim()
|
|
|
+
|
|
|
+ if (rawText) {
|
|
|
+ if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
|
|
|
+ const blockRef = rawText.slice(2, -2)
|
|
|
+ if (validUUID(blockRef)) {
|
|
|
+ newRef = blockRef
|
|
|
+ }
|
|
|
+ } else if (/^\[\[.*\]\]$/.test(rawText)) {
|
|
|
+ newRef = rawText.slice(2, -2)
|
|
|
+ }
|
|
|
}
|
|
|
- })
|
|
|
- },
|
|
|
- []
|
|
|
- )
|
|
|
+ }
|
|
|
+ if (newRef) {
|
|
|
+ app.selectedShapesArray[0].update({
|
|
|
+ refs: [newRef],
|
|
|
+ })
|
|
|
+ app.persist()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // fall through to creating shapes
|
|
|
+ }
|
|
|
+
|
|
|
+ handleCreatingShapes(app, info, handlers)
|
|
|
+ }, [])
|
|
|
}
|