Skip to content

Commit 15d5550

Browse files
committed
docs: init html content in lesson29 #65
1 parent 29b83af commit 15d5550

File tree

3 files changed

+155
-7
lines changed

3 files changed

+155
-7
lines changed

packages/site/docs/zh/guide/lesson-029.md

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,26 @@ description: ''
77

88
有时候我们希望在画布中嵌入 HTML 内容,例如 YouTube 播放器、CodeSandbox 组件、ShaderToy 等等。
99

10-
## HTML
10+
## 创建 HTML 容器 {#create-html-container}
1111

1212
Excalidraw 并不支持在画布中嵌入 HTML 内容,但 tldraw 支持 [TLEmbedShape]。它在网页中将一个 HTML 容器(含 iframe 或其他元素)和画布 `<svg>` 元素并排或叠加显示,而不是“完全”在单一画布内部。
1313

14+
HTMLContainer 的职责是把“普通 DOM”内容放到编辑器的合适层(通常是 editor 的 container),并处理与 shape 的位移/缩放/旋转同步(shape 的 transform -> DOM transform)以保证 DOM 元素在画布上的位置和 shape 对齐。
15+
16+
```css
17+
.tl-html-container {
18+
position: absolute;
19+
inset: 0px;
20+
height: 100%;
21+
width: 100%;
22+
pointer-events: none;
23+
stroke-linecap: round;
24+
stroke-linejoin: round;
25+
transform-origin: top left;
26+
color: var(--tl-color-text-1);
27+
}
28+
```
29+
1430
![HTML external content in tldraw](/html-in-tldraw.png)
1531

1632
[External content sources] 例子中,我们可以看到 tldraw 是这样支持 HTML 内容的:
@@ -41,5 +57,72 @@ export interface HtmlSerializedNode
4157
Partial<HtmlAttributes> {}
4258
```
4359

60+
## 粘贴 URL
61+
62+
[课程 24 - 读取剪贴板] 中,我们介绍过如何处理剪贴板中的图片和文本内容。
63+
64+
URL 是特殊的文本,在 tldraw 中:
65+
66+
- 当 URL 被识别为外部链接时,默认处理器会抓取页面的 metadata(og:image、title、favicon、description),并把这些信息包装成一个 bookmark asset(TLBookmarkAsset)和对应的 shape,使用书签样式渲染
67+
- 当 URL 是可嵌入内容(例如 YouTube、Figma、Google Maps 等),此时使用 `<iframe>` 渲染
68+
- 当 URL 是图片或者视频资源时,把它作为媒体 asset(TLImageAsset / TLVideoAsset)去加载并用 ImageShapeUtil 渲染
69+
70+
```ts
71+
// @see https://github.com/tldraw/tldraw/blob/main/packages/tldraw/src/lib/ui/hooks/clipboard/pasteUrl.ts#L12
72+
export async function pasteUrl() {
73+
return await editor.putExternalContent({
74+
type: 'url',
75+
point,
76+
url,
77+
sources,
78+
});
79+
}
80+
```
81+
82+
### 书签 {#bookmark}
83+
84+
```ts
85+
// @see https://github.com/tldraw/tldraw/blob/ef0eba14c5a8baf4f36b3659ac9af98256d3b5dd/packages/tldraw/src/lib/defaultExternalContentHandlers.ts#L249
86+
export async function defaultHandleExternalUrlAsset() {
87+
let meta: {
88+
image: string;
89+
favicon: string;
90+
title: string;
91+
description: string;
92+
};
93+
94+
const resp = await fetch(url, {
95+
method: 'GET',
96+
mode: 'no-cors',
97+
});
98+
const html = await resp.text();
99+
const doc = new DOMParser().parseFromString(html, 'text/html');
100+
meta = {
101+
image:
102+
doc.head
103+
.querySelector('meta[property="og:image"]')
104+
?.getAttribute('content') ?? '',
105+
// title, favicon, description
106+
};
107+
108+
// Create bookmark asset
109+
}
110+
```
111+
112+
## 粘贴 HTML 内容
113+
114+
```ts
115+
// @see https://github.com/tldraw/tldraw/blob/ef0eba14c5a8baf4f36b3659ac9af98256d3b5dd/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts#L200-L204
116+
const handlePasteFromEventClipboardData = async () => {
117+
if (item.type === 'text/html') {
118+
things.push({
119+
type: 'html',
120+
source: new Promise((r) => item.getAsString(r)) as Promise<string>,
121+
});
122+
}
123+
};
124+
```
125+
44126
[External content sources]: https://tldraw.dev/examples/external-content-sources
45127
[TLEmbedShape]: https://tldraw.dev/reference/tlschema/TLEmbedShape
128+
[课程 24 - 读取剪贴板]: /zh/guide/lesson-024#clipboard-read

packages/webcomponents/src/spectrum/context-menu.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
DOMAdapter,
1616
MIME_TYPES,
1717
ExportFormat,
18+
isUrl,
1819
} from '@infinite-canvas-tutorial/ecs';
1920
import { html, render } from '@spectrum-web-components/base';
2021
import { VirtualTrigger, openOverlay } from '@spectrum-web-components/overlay';
@@ -23,6 +24,7 @@ import { load } from '@loaders.gl/core';
2324
import { ImageLoader } from '@loaders.gl/images';
2425
import { apiContext, appStateContext } from '../context';
2526
import { ExtendedAPI } from '../API';
27+
import { extractExternalUrlMetadata } from '../utils/url';
2628

2729
const ZINDEX_OFFSET = 0.0001;
2830

@@ -241,12 +243,22 @@ export async function executePaste(
241243

242244
updateAndSelectNodes(api, appState, nodes);
243245
} else if (data.text) {
244-
// const nonEmptyLines = data.text
245-
// .replace(/\r?\n|\r/g, '\n')
246-
// .split(/\n+/)
247-
// .map((s) => s.trim())
248-
// .filter(Boolean);
249-
createText(api, appState, data.text, canvasPosition);
246+
if (isUrl(data.text)) {
247+
// TODO: youtube, figma, google maps, etc.
248+
249+
// Plain url, extract metadata
250+
const meta = await extractExternalUrlMetadata(data.text);
251+
console.log(meta);
252+
253+
// TODO: create bookmark asset
254+
} else {
255+
// const nonEmptyLines = data.text
256+
// .replace(/\r?\n|\r/g, '\n')
257+
// .split(/\n+/)
258+
// .map((s) => s.trim())
259+
// .filter(Boolean);
260+
createText(api, appState, data.text, canvasPosition);
261+
}
250262
}
251263
}
252264

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// @see https://github.com/tldraw/tldraw/blob/ef0eba14c5a8baf4f36b3659ac9af98256d3b5dd/packages/tldraw/src/lib/defaultExternalContentHandlers.ts#L249
2+
export async function extractExternalUrlMetadata(url: string) {
3+
let meta: {
4+
image: string;
5+
favicon: string;
6+
title: string;
7+
description: string;
8+
};
9+
10+
try {
11+
const resp = await fetch(url, {
12+
method: 'GET',
13+
mode: 'no-cors',
14+
});
15+
const html = await resp.text();
16+
const doc = new DOMParser().parseFromString(html, 'text/html');
17+
meta = {
18+
image:
19+
doc.head
20+
.querySelector('meta[property="og:image"]')
21+
?.getAttribute('content') ?? '',
22+
favicon:
23+
doc.head
24+
.querySelector('link[rel="apple-touch-icon"]')
25+
?.getAttribute('href') ??
26+
doc.head.querySelector('link[rel="icon"]')?.getAttribute('href') ??
27+
'',
28+
title:
29+
doc.head
30+
.querySelector('meta[property="og:title"]')
31+
?.getAttribute('content') ?? url,
32+
description:
33+
doc.head
34+
.querySelector('meta[property="og:description"]')
35+
?.getAttribute('content') ?? '',
36+
};
37+
if (!meta.image.startsWith('http')) {
38+
meta.image = new URL(meta.image, url).href;
39+
}
40+
if (!meta.favicon.startsWith('http')) {
41+
meta.favicon = new URL(meta.favicon, url).href;
42+
}
43+
} catch (error) {
44+
console.error(error);
45+
// toasts.addToast({
46+
// title: msg('assets.url.failed'),
47+
// severity: 'error',
48+
// })
49+
meta = { image: '', favicon: '', title: '', description: '' };
50+
}
51+
52+
return meta;
53+
}

0 commit comments

Comments
 (0)