mirror of https://github.com/logseq/logseq
refactor: handling DataTransfer
parent
13a98e87db
commit
803ec01b24
|
@ -61,27 +61,13 @@ interface LogseqTldrawProps {
|
||||||
onPersist?: TLReactCallbacks<Shape>['onPersist']
|
onPersist?: TLReactCallbacks<Shape>['onPersist']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const App = function App({
|
const AppInner = ({
|
||||||
onPersist,
|
onPersist,
|
||||||
handlers,
|
|
||||||
renderers,
|
|
||||||
model,
|
model,
|
||||||
...rest
|
...rest
|
||||||
}: LogseqTldrawProps): JSX.Element {
|
}: Omit<LogseqTldrawProps, 'renderers' | 'handlers'>) => {
|
||||||
const memoRenders: any = React.useMemo(() => {
|
const onDrop = useDrop()
|
||||||
return Object.fromEntries(
|
const onPaste = usePaste()
|
||||||
Object.entries(renderers).map(([key, comp]) => {
|
|
||||||
return [key, React.memo(comp)]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}, [])
|
|
||||||
const contextValue = {
|
|
||||||
renderers: memoRenders,
|
|
||||||
handlers: handlers,
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDrop = useDrop(contextValue)
|
|
||||||
const onPaste = usePaste(contextValue)
|
|
||||||
const onQuickAdd = useQuickAdd()
|
const onQuickAdd = useQuickAdd()
|
||||||
const ref = React.useRef<HTMLDivElement>(null)
|
const ref = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
@ -95,7 +81,6 @@ export const App = function App({
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogseqContext.Provider value={contextValue}>
|
|
||||||
<AppProvider
|
<AppProvider
|
||||||
Shapes={shapes}
|
Shapes={shapes}
|
||||||
Tools={tools}
|
Tools={tools}
|
||||||
|
@ -114,6 +99,25 @@ export const App = function App({
|
||||||
</div>
|
</div>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const App = function App({ renderers, handlers, ...rest }: LogseqTldrawProps): JSX.Element {
|
||||||
|
const memoRenders: any = React.useMemo(() => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(renderers).map(([key, comp]) => {
|
||||||
|
return [key, React.memo(comp)]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
const contextValue = {
|
||||||
|
renderers: memoRenders,
|
||||||
|
handlers: handlers,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LogseqContext.Provider value={contextValue}>
|
||||||
|
<AppInner {...rest} />
|
||||||
</LogseqContext.Provider>
|
</LogseqContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,11 @@ const HistoryStack = observer(function HistoryStack() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
anchorRef.current?.querySelector(`[data-item-index="${app.history.pointer}"]`)?.scrollIntoView()
|
requestIdleCallback(() => {
|
||||||
|
anchorRef.current
|
||||||
|
?.querySelector(`[data-item-index="${app.history.pointer}"]`)
|
||||||
|
?.scrollIntoView()
|
||||||
|
})
|
||||||
}, [app.history.pointer])
|
}, [app.history.pointer])
|
||||||
|
|
||||||
return anchorRef.current
|
return anchorRef.current
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import type { TLReactCallbacks } from '@tldraw/react'
|
import type { TLReactCallbacks } from '@tldraw/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { Shape } from '../lib'
|
import type { Shape } from '../lib'
|
||||||
import type { LogseqContextValue } from '../lib/logseq-context'
|
|
||||||
import { usePaste } from './usePaste'
|
import { usePaste } from './usePaste'
|
||||||
|
|
||||||
export function useDrop(context: LogseqContextValue) {
|
export function useDrop() {
|
||||||
const handlePaste = usePaste(context)
|
const handlePaste = usePaste()
|
||||||
return React.useCallback<TLReactCallbacks<Shape>['onDrop']>(async (app, { dataTransfer, point }) => {
|
return React.useCallback<TLReactCallbacks<Shape>['onDrop']>(
|
||||||
|
async (app, { dataTransfer, point }) => {
|
||||||
handlePaste(app, { point, shiftKey: false, dataTransfer })
|
handlePaste(app, { point, shiftKey: false, dataTransfer })
|
||||||
}, [])
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Item, Label } from '@radix-ui/react-select'
|
|
||||||
import {
|
import {
|
||||||
BoundsUtils,
|
BoundsUtils,
|
||||||
getSizeFromSrc,
|
getSizeFromSrc,
|
||||||
|
isNonNullable,
|
||||||
TLAsset,
|
TLAsset,
|
||||||
TLBinding,
|
TLBinding,
|
||||||
TLCursor,
|
TLCursor,
|
||||||
|
@ -14,17 +14,15 @@ import Vec from '@tldraw/vec'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { NIL as NIL_UUID } from 'uuid'
|
import { NIL as NIL_UUID } from 'uuid'
|
||||||
import {
|
import {
|
||||||
type Shape,
|
|
||||||
HTMLShape,
|
HTMLShape,
|
||||||
YouTubeShape,
|
IFrameShape,
|
||||||
|
ImageShape,
|
||||||
LogseqPortalShape,
|
LogseqPortalShape,
|
||||||
VideoShape,
|
VideoShape,
|
||||||
TextShape,
|
YouTubeShape,
|
||||||
LineShape,
|
type Shape,
|
||||||
ImageShape,
|
|
||||||
IFrameShape,
|
|
||||||
} from '../lib'
|
} from '../lib'
|
||||||
import type { LogseqContextValue } from '../lib/logseq-context'
|
import { LogseqContext } from '../lib/logseq-context'
|
||||||
|
|
||||||
const isValidURL = (url: string) => {
|
const isValidURL = (url: string) => {
|
||||||
try {
|
try {
|
||||||
|
@ -43,36 +41,64 @@ const safeParseJson = (json: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VideoImageAsset extends TLAsset {
|
||||||
|
size?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
|
const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
|
||||||
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg']
|
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg']
|
||||||
|
|
||||||
|
function getFileType(filename: string) {
|
||||||
|
// Get extension, verify that it's an image
|
||||||
|
const extensionMatch = filename.match(/\.[0-9a-z]+$/i)
|
||||||
|
if (!extensionMatch) {
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
const extension = extensionMatch[0].toLowerCase()
|
||||||
|
if (IMAGE_EXTENSIONS.includes(extension)) {
|
||||||
|
return 'image'
|
||||||
|
}
|
||||||
|
if (VIDEO_EXTENSIONS.includes(extension)) {
|
||||||
|
return 'video'
|
||||||
|
}
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
type MaybeShapes = Shape['props'][] | null | undefined
|
||||||
|
|
||||||
|
type CreateShapeFN<Args extends any[]> = (...args: Args) => Promise<MaybeShapes> | MaybeShapes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try create a shape from a list of create shape functions. If one of the functions returns a
|
||||||
|
* shape, return it, otherwise try again for the next one until all have been tried.
|
||||||
|
*/
|
||||||
|
async function tryCreateShapeHelper<Args extends any[]>(fns: CreateShapeFN<Args>[], ...args: Args) {
|
||||||
|
for (const fn of fns) {
|
||||||
|
const result = await fn(...(args as any))
|
||||||
|
if (result && result.length > 0) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: for assets, we should prompt the user a loading spinner
|
// FIXME: for assets, we should prompt the user a loading spinner
|
||||||
export function usePaste(context: LogseqContextValue) {
|
export function usePaste() {
|
||||||
const { handlers } = context
|
const { handlers } = React.useContext(LogseqContext)
|
||||||
|
|
||||||
return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(
|
return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(
|
||||||
async (app, { point, shiftKey, dataTransfer }) => {
|
async (app, { point, shiftKey, dataTransfer }) => {
|
||||||
interface VideoImageAsset extends TLAsset {
|
let imageAssetsToCreate: VideoImageAsset[] = []
|
||||||
size: number[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageAssetsToCreate: VideoImageAsset[] = []
|
|
||||||
let assetsToClone: TLAsset[] = []
|
let assetsToClone: TLAsset[] = []
|
||||||
const shapesToCreate: Shape['props'][] = []
|
const shapesToCreate: Shape['props'][] = []
|
||||||
const bindingsToCreate: TLBinding[] = []
|
const bindingsToCreate: TLBinding[] = []
|
||||||
|
|
||||||
async function createAsset(file: File): Promise<string | null> {
|
async function createAssetsFromURL(url: string, isVideo: boolean): Promise<VideoImageAsset> {
|
||||||
return await handlers.saveAsset(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAssetUrl(url: string, isVideo: boolean) {
|
|
||||||
// Do we already have an asset for this image?
|
// Do we already have an asset for this image?
|
||||||
const existingAsset = Object.values(app.assets).find(asset => asset.src === url)
|
const existingAsset = Object.values(app.assets).find(asset => asset.src === url)
|
||||||
if (existingAsset) {
|
if (existingAsset) {
|
||||||
imageAssetsToCreate.push(existingAsset as VideoImageAsset)
|
return existingAsset as VideoImageAsset
|
||||||
return true
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
|
||||||
// Create a new asset for this image
|
// Create a new asset for this image
|
||||||
const asset: VideoImageAsset = {
|
const asset: VideoImageAsset = {
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
|
@ -80,160 +106,153 @@ export function usePaste(context: LogseqContextValue) {
|
||||||
src: url,
|
src: url,
|
||||||
size: await getSizeFromSrc(handlers.makeAssetUrl(url), isVideo),
|
size: await getSizeFromSrc(handlers.makeAssetUrl(url), isVideo),
|
||||||
}
|
}
|
||||||
imageAssetsToCreate.push(asset)
|
return asset
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: handle PDF?
|
async function createAssetsFromFiles(files: File[]) {
|
||||||
async function handleFiles(files: File[]) {
|
const tasks = files
|
||||||
let added = false
|
.filter(file => getFileType(file.name) !== 'unknown')
|
||||||
for (const file of files) {
|
.map(async file => {
|
||||||
// Get extension, verify that it's an image
|
|
||||||
const extensionMatch = file.name.match(/\.[0-9a-z]+$/i)
|
|
||||||
if (!extensionMatch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const extension = extensionMatch[0].toLowerCase()
|
|
||||||
if (![...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS].includes(extension)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const isVideo = VIDEO_EXTENSIONS.includes(extension)
|
|
||||||
try {
|
try {
|
||||||
// Turn the image into a base64 dataurl
|
const dataurl = await handlers.saveAsset(file)
|
||||||
const dataurl = await createAsset(file)
|
return await createAssetsFromURL(dataurl, getFileType(file.name) === 'video')
|
||||||
if (!dataurl) {
|
} catch (err) {
|
||||||
continue
|
console.error(err)
|
||||||
}
|
|
||||||
if (await handleAssetUrl(dataurl, isVideo)) {
|
|
||||||
added = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return added
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleItems(items: any) {
|
|
||||||
for (const item of items) {
|
|
||||||
if (await handleDroppedItem(item)) {
|
|
||||||
const lineId = uniqueId()
|
|
||||||
|
|
||||||
const startBinding: TLBinding = {
|
|
||||||
id: uniqueId(),
|
|
||||||
distance: 200,
|
|
||||||
handleId: 'start',
|
|
||||||
fromId: lineId,
|
|
||||||
toId: app.selectedShapesArray[app.selectedShapesArray.length - 1].id,
|
|
||||||
point: [point[0], point[1]],
|
|
||||||
}
|
|
||||||
bindingsToCreate.push(startBinding)
|
|
||||||
|
|
||||||
const endBinding: TLBinding = {
|
|
||||||
id: uniqueId(),
|
|
||||||
distance: 200,
|
|
||||||
handleId: 'end',
|
|
||||||
fromId: lineId,
|
|
||||||
toId: shapesToCreate[shapesToCreate.length - 1].id,
|
|
||||||
point: [point[0], point[1]],
|
|
||||||
}
|
|
||||||
bindingsToCreate.push(endBinding)
|
|
||||||
|
|
||||||
shapesToCreate.push({
|
|
||||||
...LineShape.defaultProps,
|
|
||||||
id: lineId,
|
|
||||||
handles: {
|
|
||||||
start: { id: 'start', canBind: true, point: app.selectedShapesArray[0].getCenter(), bindingId: startBinding.id },
|
|
||||||
end: { id: 'end', canBind: true, point: [point[0], point[1]], bindingId: endBinding.id },
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
|
return (await Promise.all(tasks)).filter(isNonNullable)
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTransfer(dataTransfer: DataTransfer) {
|
function createHTMLShape(text: string) {
|
||||||
let added = false
|
return {
|
||||||
|
|
||||||
if ((dataTransfer.files?.length && await handleFiles(Array.from(dataTransfer.files))) ||
|
|
||||||
(dataTransfer.items?.length && await handleItems(Array.from(dataTransfer.items).map((item: any) => ({type: item.type, text: dataTransfer.getData(item.type)})))) ) {
|
|
||||||
added = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return added
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleHTML(item: ClipboardItem) {
|
|
||||||
if (item.types.includes('text/html')) {
|
|
||||||
const blob = await item.getType('text/html')
|
|
||||||
const rawText = (await blob.text()).trim()
|
|
||||||
|
|
||||||
shapesToCreate.push({
|
|
||||||
...HTMLShape.defaultProps,
|
...HTMLShape.defaultProps,
|
||||||
html: rawText,
|
html: text,
|
||||||
point: [point[0], point[1]],
|
point: [point[0], point[1]],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// async function handleItems(items: any) {
|
||||||
|
// for (const item of items) {
|
||||||
|
// if (await handleDroppedItem(item)) {
|
||||||
|
// const lineId = uniqueId()
|
||||||
|
|
||||||
|
// const startBinding: TLBinding = {
|
||||||
|
// id: uniqueId(),
|
||||||
|
// distance: 200,
|
||||||
|
// handleId: 'start',
|
||||||
|
// fromId: lineId,
|
||||||
|
// toId: app.selectedShapesArray[app.selectedShapesArray.length - 1].id,
|
||||||
|
// point: [point[0], point[1]],
|
||||||
|
// }
|
||||||
|
// bindingsToCreate.push(startBinding)
|
||||||
|
|
||||||
|
// const endBinding: TLBinding = {
|
||||||
|
// id: uniqueId(),
|
||||||
|
// distance: 200,
|
||||||
|
// handleId: 'end',
|
||||||
|
// fromId: lineId,
|
||||||
|
// toId: shapesToCreate[shapesToCreate.length - 1].id,
|
||||||
|
// point: [point[0], point[1]],
|
||||||
|
// }
|
||||||
|
// bindingsToCreate.push(endBinding)
|
||||||
|
|
||||||
|
// shapesToCreate.push({
|
||||||
|
// ...LineShape.defaultProps,
|
||||||
|
// id: lineId,
|
||||||
|
// handles: {
|
||||||
|
// start: {
|
||||||
|
// id: 'start',
|
||||||
|
// canBind: true,
|
||||||
|
// point: app.selectedShapesArray[0].getCenter(),
|
||||||
|
// bindingId: startBinding.id,
|
||||||
|
// },
|
||||||
|
// end: {
|
||||||
|
// id: 'end',
|
||||||
|
// canBind: true,
|
||||||
|
// point: [point[0], point[1]],
|
||||||
|
// bindingId: endBinding.id,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
async function tryCreateShapesFromDataTransfer(dataTransfer: DataTransfer) {
|
||||||
|
return tryCreateShapeHelper(
|
||||||
|
[tryCreateShapeFromFiles, tryCreateShapeFromTextHTML, tryCreateShapeFromTextPlain],
|
||||||
|
dataTransfer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryCreateShapeFromFiles(dataTransfer: DataTransfer) {
|
||||||
|
const files = Array.from(dataTransfer.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,
|
||||||
|
// TODO: Should be place near the last edited shape
|
||||||
|
assetId: asset.id,
|
||||||
|
opacity: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDroppedItem(item: any) {
|
return newShape
|
||||||
switch(item.type) {
|
|
||||||
case 'text/html':
|
|
||||||
shapesToCreate.push({
|
|
||||||
...HTMLShape.defaultProps,
|
|
||||||
html: item.text,
|
|
||||||
point: [point[0], point[1]],
|
|
||||||
})
|
})
|
||||||
return true
|
}
|
||||||
case 'text/plain':
|
return null
|
||||||
if (await handleURL(item.text)) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shapesToCreate.push({
|
function tryCreateShapeFromTextHTML(dataTransfer: DataTransfer) {
|
||||||
...TextShape.defaultProps,
|
if (dataTransfer.types.includes('text/html') && !shiftKey) {
|
||||||
text: item.text,
|
const html = dataTransfer.getData('text/html')
|
||||||
point: [point[0], point[1]],
|
|
||||||
})
|
if (html) {
|
||||||
return true
|
return [createHTMLShape(html)]
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
async function handleTextPlain(item: ClipboardItem) {
|
async function tryCreateShapeFromTextPlain(dataTransfer: DataTransfer) {
|
||||||
if (item.types.includes('text/plain')) {
|
if (dataTransfer.types.includes('text/plain')) {
|
||||||
const blob = await item.getType('text/plain')
|
const text = dataTransfer.getData('text/plain').trim()
|
||||||
const rawText = (await blob.text()).trim()
|
|
||||||
|
|
||||||
if (await handleURL(rawText)) {
|
return tryCreateShapeHelper(
|
||||||
return true
|
[
|
||||||
|
tryCreateShapeFromURL,
|
||||||
|
tryCreateShapeFromIframeString,
|
||||||
|
tryCreateClonedShapesFromJSON,
|
||||||
|
tryCreateLogseqPortalShapesFromString,
|
||||||
|
],
|
||||||
|
text
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleIframe(rawText)) {
|
return null
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleTldrawShapes(rawText)) {
|
function tryCreateClonedShapesFromJSON(rawText: string) {
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (await handleLogseqPortalShapes(rawText)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTldrawShapes(rawText: string) {
|
|
||||||
const data = safeParseJson(rawText)
|
const data = safeParseJson(rawText)
|
||||||
try {
|
try {
|
||||||
if (data?.type === 'logseq/whiteboard-shapes') {
|
if (data?.type === 'logseq/whiteboard-shapes') {
|
||||||
|
@ -249,7 +268,7 @@ export function usePaste(context: LogseqContextValue) {
|
||||||
maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
|
maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
const clonedShapes = shapes.map(shape => {
|
const shapesToCreate = shapes.map(shape => {
|
||||||
return {
|
return {
|
||||||
...shape,
|
...shape,
|
||||||
point: [
|
point: [
|
||||||
|
@ -258,14 +277,14 @@ export function usePaste(context: LogseqContextValue) {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// @ts-expect-error - This is a valid shape
|
|
||||||
shapesToCreate.push(...clonedShapes)
|
|
||||||
|
|
||||||
// Try to rebinding the shapes to the new assets
|
// Try to rebinding the shapes to the new assets
|
||||||
shapesToCreate.forEach((s, idx) => {
|
shapesToCreate
|
||||||
if (s.handles) {
|
.flatMap(s => Object.values(s.handles ?? {}))
|
||||||
Object.values(s.handles).forEach(h => {
|
.forEach(h => {
|
||||||
if (h.bindingId) {
|
if (!h.bindingId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// try to bind the new shape
|
// try to bind the new shape
|
||||||
const binding = app.currentPage.bindings[h.bindingId]
|
const binding = app.currentPage.bindings[h.bindingId]
|
||||||
// FIXME: if copy from a different whiteboard, the binding info
|
// FIXME: if copy from a different whiteboard, the binding info
|
||||||
|
@ -286,21 +305,20 @@ export function usePaste(context: LogseqContextValue) {
|
||||||
} else {
|
} else {
|
||||||
h.bindingId = undefined
|
h.bindingId = undefined
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
console.warn('binding not found', h.bindingId)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return shapesToCreate as Shape['props'][]
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleURL(rawText: string) {
|
async function tryCreateShapeFromURL(rawText: string) {
|
||||||
if (isValidURL(rawText)) {
|
if (isValidURL(rawText)) {
|
||||||
const isYoutubeUrl = (url: string) => {
|
const isYoutubeUrl = (url: string) => {
|
||||||
const youtubeRegex =
|
const youtubeRegex =
|
||||||
|
@ -308,116 +326,108 @@ export function usePaste(context: LogseqContextValue) {
|
||||||
return youtubeRegex.test(url)
|
return youtubeRegex.test(url)
|
||||||
}
|
}
|
||||||
if (isYoutubeUrl(rawText)) {
|
if (isYoutubeUrl(rawText)) {
|
||||||
shapesToCreate.push({
|
return [
|
||||||
|
{
|
||||||
...YouTubeShape.defaultProps,
|
...YouTubeShape.defaultProps,
|
||||||
url: rawText,
|
url: rawText,
|
||||||
point: [point[0], point[1]],
|
point: [point[0], point[1]],
|
||||||
})
|
},
|
||||||
return true
|
]
|
||||||
}
|
|
||||||
const extension = rawText.match(/\.[0-9a-z]+$/i)?.[0].toLowerCase()
|
|
||||||
if (
|
|
||||||
extension &&
|
|
||||||
[...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS].includes(extension) &&
|
|
||||||
(await handleAssetUrl(rawText, VIDEO_EXTENSIONS.includes(extension)))
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shapesToCreate.push({
|
return [
|
||||||
|
{
|
||||||
...IFrameShape.defaultProps,
|
...IFrameShape.defaultProps,
|
||||||
url: rawText,
|
url: rawText,
|
||||||
point: [point[0], point[1]],
|
point: [point[0], point[1]],
|
||||||
})
|
},
|
||||||
return true
|
]
|
||||||
}
|
}
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleIframe(rawText: string) {
|
function tryCreateShapeFromIframeString(rawText: string) {
|
||||||
// if rawText is iframe text
|
// if rawText is iframe text
|
||||||
if (rawText.startsWith('<iframe')) {
|
if (rawText.startsWith('<iframe')) {
|
||||||
shapesToCreate.push({
|
return [
|
||||||
|
{
|
||||||
...HTMLShape.defaultProps,
|
...HTMLShape.defaultProps,
|
||||||
html: rawText,
|
html: rawText,
|
||||||
point: [point[0], point[1]],
|
point: [point[0], point[1]],
|
||||||
})
|
},
|
||||||
return true
|
]
|
||||||
}
|
}
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogseqPortalShapes(rawText: string) {
|
async function tryCreateLogseqPortalShapesFromString(rawText: string) {
|
||||||
if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
|
if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
|
||||||
const blockRef = rawText.slice(2, -2)
|
const blockRef = rawText.slice(2, -2)
|
||||||
if (validUUID(blockRef)) {
|
if (validUUID(blockRef)) {
|
||||||
shapesToCreate.push({
|
return [
|
||||||
|
{
|
||||||
...LogseqPortalShape.defaultProps,
|
...LogseqPortalShape.defaultProps,
|
||||||
point: [point[0], point[1]],
|
point: [point[0], point[1]],
|
||||||
size: [400, 0], // use 0 here to enable auto-resize
|
size: [400, 0], // use 0 here to enable auto-resize
|
||||||
pageId: blockRef,
|
pageId: blockRef,
|
||||||
blockType: 'B',
|
blockType: 'B' as 'B',
|
||||||
})
|
},
|
||||||
return true
|
]
|
||||||
}
|
}
|
||||||
} else if (/^\[\[.*\]\]$/.test(rawText)) {
|
}
|
||||||
|
// [[page name]] ?
|
||||||
|
else if (/^\[\[.*\]\]$/.test(rawText)) {
|
||||||
const pageName = rawText.slice(2, -2)
|
const pageName = rawText.slice(2, -2)
|
||||||
shapesToCreate.push({
|
return [
|
||||||
|
{
|
||||||
...LogseqPortalShape.defaultProps,
|
...LogseqPortalShape.defaultProps,
|
||||||
point: [point[0], point[1]],
|
point: [point[0], point[1]],
|
||||||
size: [400, 0], // use 0 here to enable auto-resize
|
size: [400, 0], // use 0 here to enable auto-resize
|
||||||
pageId: pageName,
|
pageId: pageName,
|
||||||
blockType: 'P',
|
blockType: 'P' as 'P',
|
||||||
})
|
},
|
||||||
return true
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise, creating a new block that belongs to the current whiteboard
|
||||||
const uuid = handlers?.addNewBlock(rawText)
|
const uuid = handlers?.addNewBlock(rawText)
|
||||||
if (uuid) {
|
if (uuid) {
|
||||||
// create text shape
|
// create text shape
|
||||||
shapesToCreate.push({
|
return [
|
||||||
|
{
|
||||||
...LogseqPortalShape.defaultProps,
|
...LogseqPortalShape.defaultProps,
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
size: [400, 0], // use 0 here to enable auto-resize
|
size: [400, 0], // use 0 here to enable auto-resize
|
||||||
point: [point[0], point[1]],
|
point: [point[0], point[1]],
|
||||||
pageId: uuid,
|
pageId: uuid,
|
||||||
blockType: 'B',
|
blockType: 'B' as 'B',
|
||||||
compact: true,
|
compact: true,
|
||||||
})
|
},
|
||||||
return true
|
]
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
app.cursors.setCursor(TLCursor.Progress)
|
app.cursors.setCursor(TLCursor.Progress)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (dataTransfer) {
|
const shapesFromDataTransfer = dataTransfer
|
||||||
await handleTransfer(dataTransfer)
|
? await tryCreateShapesFromDataTransfer(dataTransfer)
|
||||||
|
: null
|
||||||
|
if (shapesFromDataTransfer) {
|
||||||
|
shapesToCreate.push(...shapesFromDataTransfer)
|
||||||
} else {
|
} else {
|
||||||
for (const item of await navigator.clipboard.read()) {
|
// from Clipboard app or Shift copy etc
|
||||||
let handled = !shiftKey ? await handleHTML(item) : false
|
// in this case, we do not have the dataTransfer object
|
||||||
if (!handled) {
|
|
||||||
await handleTextPlain(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const allShapesToAdd: TLShapeModel[] = [
|
console.log(bindingsToCreate)
|
||||||
// assets to images
|
|
||||||
...imageAssetsToCreate.map((asset, i) => ({
|
const allShapesToAdd: TLShapeModel[] = shapesToCreate.map(shape => {
|
||||||
...(asset.type === 'video' ? VideoShape : ImageShape).defaultProps,
|
|
||||||
// TODO: Should be place near the last edited shape
|
|
||||||
point: [point[0] - asset.size[0] / 4 + i * 16, point[1] - asset.size[1] / 4 + i * 16],
|
|
||||||
size: Vec.div(asset.size, 2),
|
|
||||||
assetId: asset.id,
|
|
||||||
opacity: 1,
|
|
||||||
})),
|
|
||||||
...shapesToCreate,
|
|
||||||
].map(shape => {
|
|
||||||
return {
|
return {
|
||||||
...shape,
|
...shape,
|
||||||
parentId: app.currentPageId,
|
parentId: app.currentPageId,
|
||||||
|
|
|
@ -36,4 +36,4 @@ export interface LogseqContextValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LogseqContext = React.createContext<Partial<LogseqContextValue>>({})
|
export const LogseqContext = React.createContext<LogseqContextValue>({} as LogseqContextValue)
|
||||||
|
|
|
@ -105,8 +105,7 @@ const useSearch = (q: string, searchFilter: 'B' | 'P' | null) => {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let canceled = false
|
let canceled = false
|
||||||
const searchHandler = handlers?.search
|
if (q.length > 0) {
|
||||||
if (q.length > 0 && searchHandler) {
|
|
||||||
const filter = { 'pages?': true, 'blocks?': true, 'files?': false }
|
const filter = { 'pages?': true, 'blocks?': true, 'files?': false }
|
||||||
if (searchFilter === 'B') {
|
if (searchFilter === 'B') {
|
||||||
filter['pages?'] = false
|
filter['pages?'] = false
|
||||||
|
|
|
@ -439,7 +439,7 @@ export class TLApp<
|
||||||
this.notify('paste', {
|
this.notify('paste', {
|
||||||
point: this.inputs.currentPoint,
|
point: this.inputs.currentPoint,
|
||||||
shiftKey: !!shiftKey,
|
shiftKey: !!shiftKey,
|
||||||
dataTransfer: e?.clipboardData,
|
dataTransfer: e?.clipboardData ?? undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue