instant bookmarks (#2176)
this PR does a couple of things when creating bookmarks - any time a url was pasted it was previously calling `fetch` on the url to check whether the url is an image that we have cors access to. In that case we can paste the image itself rather than a bookmark. But that's gonna be a relatively rare use case, and the check itself seemed to cost anywhere from 200ms to +1s which is certainly not worth it when the fallback behaviour (create a regular bookmark) is fine. So i moved that check behind a url pathname extension check. i.e. if the url pathname ends with .gif, .jpg, .jpeg, .svg, or .png, then it will check whether we can paste the image directly, otherwise it will always do a regular bookmark. - we create an asset-less bookmark shape on the canvas while we wait for the asset to load if it is not already available. This means the user gets immediate feedback that their paste succeeded, but they won't see the actual bookmark details for a little bit. It looks like this ![Kapture 2023-11-08 at 10 34 35](https://github.com/tldraw/tldraw/assets/1242537/89c93612-a794-419f-aa68-1efdb82bfbf2) ### Change Type - [x] `minor` — New feature ### Release Notes - Improves ux around pasting bookmarks
This commit is contained in:
parent
8dceb5ee31
commit
133b8fbdc0
2 changed files with 83 additions and 33 deletions
|
@ -4,7 +4,9 @@ import {
|
|||
MediaHelpers,
|
||||
TLAsset,
|
||||
TLAssetId,
|
||||
TLBookmarkShape,
|
||||
TLEmbedShape,
|
||||
TLShapeId,
|
||||
TLShapePartial,
|
||||
TLTextShape,
|
||||
TLTextShapeProps,
|
||||
|
@ -354,6 +356,7 @@ export function registerDefaultExternalContentHandlers(
|
|||
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
|
||||
|
||||
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
|
||||
const shape = createEmptyBookmarkShape(editor, url, position)
|
||||
|
||||
// Use an existing asset if we have one, or else else create a new one
|
||||
let asset = editor.getAsset(assetId) as TLAsset
|
||||
|
@ -370,13 +373,25 @@ export function registerDefaultExternalContentHandlers(
|
|||
editor.createAssets([asset])
|
||||
}
|
||||
|
||||
createShapesForAssets(editor, [asset], position)
|
||||
editor.updateShapes([
|
||||
{
|
||||
...shape,
|
||||
props: {
|
||||
...shape.props,
|
||||
assetId: asset.id,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function createShapesForAssets(editor: Editor, assets: TLAsset[], position: VecLike) {
|
||||
if (!assets.length) return
|
||||
export async function createShapesForAssets(
|
||||
editor: Editor,
|
||||
assets: TLAsset[],
|
||||
position: VecLike
|
||||
): Promise<TLShapeId[]> {
|
||||
if (!assets.length) return []
|
||||
|
||||
const currentPoint = Vec2d.From(position)
|
||||
const partials: TLShapePartial[] = []
|
||||
|
@ -445,6 +460,14 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p
|
|||
// Create the shapes
|
||||
editor.createShapes(partials).select(...partials.map((p) => p.id))
|
||||
|
||||
// Re-position shapes so that the center of the group is at the provided point
|
||||
centerSelecitonAroundPoint(editor, position)
|
||||
})
|
||||
|
||||
return partials.map((p) => p.id)
|
||||
}
|
||||
|
||||
function centerSelecitonAroundPoint(editor: Editor, position: VecLike) {
|
||||
// Re-position shapes so that the center of the group is at the provided point
|
||||
const { viewportPageBounds } = editor
|
||||
let { selectionPageBounds } = editor
|
||||
|
@ -471,5 +494,29 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p
|
|||
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
|
||||
editor.zoomToSelection()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function createEmptyBookmarkShape(
|
||||
editor: Editor,
|
||||
url: string,
|
||||
position: VecLike
|
||||
): TLBookmarkShape {
|
||||
const partial: TLShapePartial = {
|
||||
id: createShapeId(),
|
||||
type: 'bookmark',
|
||||
x: position.x - 150,
|
||||
y: position.y - 160,
|
||||
opacity: 1,
|
||||
props: {
|
||||
assetId: null,
|
||||
url,
|
||||
},
|
||||
}
|
||||
|
||||
editor.batch(() => {
|
||||
editor.createShapes([partial]).select(partial.id)
|
||||
centerSelecitonAroundPoint(editor, position)
|
||||
})
|
||||
|
||||
return editor.getShape(partial.id) as TLBookmarkShape
|
||||
}
|
||||
|
|
|
@ -18,12 +18,15 @@ export async function pasteUrl(
|
|||
) {
|
||||
// Lets see if its an image and we have CORs
|
||||
try {
|
||||
const resp = await fetch(url)
|
||||
// skip this step if the url doesn't contain an image extension, treat it as a regular bookmark
|
||||
if (new URL(url).pathname.match(/\.(png|jpe?g|gif|svg|webp)$/i)) {
|
||||
const resp = await fetch(url, { method: 'HEAD' })
|
||||
if (resp.headers.get('content-type')?.match(/^image\//)) {
|
||||
editor.mark('paste')
|
||||
pasteFiles(editor, [url])
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message !== 'Failed to fetch') {
|
||||
console.error(err)
|
||||
|
|
Loading…
Reference in a new issue