examples: clean up Canvas/Store events and make UiEvents have code snippets (#2770)

Fixes https://linear.app/tldraw/issue/TLD-2059

<img width="1220" alt="Screenshot 2024-02-07 at 12 38 09"
src="https://github.com/tldraw/tldraw/assets/469604/15dc4298-670d-489b-8bee-810d34a0fbae">


### Change Type

- [x] `internal` — Any other changes that don't affect the published
package[^2]

### Release Notes

- Examples: add an interactive example that shows code snippets for the
SDK.
pull/2814/head
Mime Čuvalo 2024-02-07 16:51:04 +00:00 zatwierdzone przez GitHub
rodzic e2a03abf5c
commit f16e597761
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
13 zmienionych plików z 273 dodań i 22 usunięć

Wyświetl plik

@ -40,12 +40,14 @@
"@vercel/analytics": "^1.1.1",
"classnames": "^2.3.2",
"lazyrepo": "0.0.0-alpha.27",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.17.0",
"vite": "^5.0.0"
},
"devDependencies": {
"@types/lodash": "^4.14.188",
"@vitejs/plugin-react": "^4.2.0",
"dotenv": "^16.3.1",
"remark": "^15.0.1",

Wyświetl plik

@ -64,6 +64,11 @@ export function ExamplePage({
</a>
</div>
</div>
<div className="example__sidebar__header-links">
<a className="example__sidebar__header-link" href="/develop">
Develop
</a>
</div>
<ul className="example__sidebar__categories scroll-light">
{categories.map((currentCategory) => (
<li key={currentCategory} className="example__sidebar__category">

Wyświetl plik

@ -5,10 +5,23 @@ import { useCallback, useState } from 'react'
// There's a guide at the bottom of this file!
export default function CanvasEventsExample() {
const [events, setEvents] = useState<string[]>([])
const [events, setEvents] = useState<any[]>([])
const handleEvent = useCallback((data: TLEventInfo) => {
setEvents((events) => [JSON.stringify(data, null, '\t'), ...events.slice(0, 100)])
setEvents((events) => {
const newEvents = events.slice(0, 100)
if (
newEvents[newEvents.length - 1] &&
newEvents[newEvents.length - 1].type === 'pointer' &&
data.type === 'pointer' &&
data.target === 'canvas'
) {
newEvents[newEvents.length - 1] = data
} else {
newEvents.unshift(data)
}
return newEvents
})
}, [])
return (
@ -36,9 +49,7 @@ export default function CanvasEventsExample() {
whiteSpace: 'pre-wrap',
}}
>
{events.map((t, i) => (
<div key={i}>{t}</div>
))}
<div>{JSON.stringify(events, undefined, 2)}</div>
</div>
</div>
)

Wyświetl plik

@ -1,5 +1,6 @@
import { Editor, TLEventMapHandler, Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import _ from 'lodash'
import { useCallback, useEffect, useState } from 'react'
// There's a guide at the bottom of this file!
@ -17,7 +18,7 @@ export default function StoreEventsExample() {
if (!editor) return
function logChangeEvent(eventName: string) {
setStoreEvents((events) => [eventName, ...events])
setStoreEvents((events) => [...events, eventName])
}
//[1]
@ -25,7 +26,7 @@ export default function StoreEventsExample() {
// Added
for (const record of Object.values(change.changes.added)) {
if (record.typeName === 'shape') {
logChangeEvent(`created shape (${record.type})`)
logChangeEvent(`created shape (${record.type})\n`)
}
}
@ -37,13 +38,29 @@ export default function StoreEventsExample() {
from.currentPageId !== to.currentPageId
) {
logChangeEvent(`changed page (${from.currentPageId}, ${to.currentPageId})`)
} else if (from.id.startsWith('shape') && to.id.startsWith('shape')) {
let diff = _.reduce(
from,
(result: any[], value, key: string) =>
_.isEqual(value, (to as any)[key]) ? result : result.concat([key, value]),
[]
)
if (diff?.[0] === 'props') {
diff = _.reduce(
(from as any).props,
(result: any[], value, key) =>
_.isEqual(value, (to as any).props[key]) ? result : result.concat([key, value]),
[]
)
}
logChangeEvent(`updated shape (${JSON.stringify(diff)})\n`)
}
}
// Removed
for (const record of Object.values(change.changes.removed)) {
if (record.typeName === 'shape') {
logChangeEvent(`deleted shape (${record.type})`)
logChangeEvent(`deleted shape (${record.type})\n`)
}
}
}
@ -76,9 +93,7 @@ export default function StoreEventsExample() {
overflow: 'auto',
}}
>
{storeEvents.map((t, i) => (
<div key={i}>{t}</div>
))}
<pre>{storeEvents}</pre>
</div>
</div>
)

Wyświetl plik

@ -1,14 +1,19 @@
import { TLUiEventHandler, Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { useCallback, useState } from 'react'
import { Fragment, useCallback, useState } from 'react'
import { getCodeSnippet } from './codeSnippets'
// There's a guide at the bottom of this file!
export default function UiEventsExample() {
const [uiEvents, setUiEvents] = useState<string[]>([])
const handleUiEvent = useCallback<TLUiEventHandler>((name, data) => {
setUiEvents((events) => [`${name} ${JSON.stringify(data)}`, ...events])
const handleUiEvent = useCallback<TLUiEventHandler>((name, data: any) => {
const codeSnippet = getCodeSnippet(name, data)
setUiEvents((events) => [
`event: ${name} ${JSON.stringify(data)}${codeSnippet && `\ncode: ${codeSnippet}`}`,
...events,
])
}, [])
return (
@ -32,7 +37,11 @@ export default function UiEventsExample() {
}}
>
{uiEvents.map((t, i) => (
<div key={i}>{t}</div>
<Fragment key={i}>
<pre style={{ borderBottom: '1px solid #000', marginBottom: 0, paddingBottom: '12px' }}>
{t}
</pre>
</Fragment>
))}
</div>
</div>
@ -45,6 +54,9 @@ grouping shapes, zooming etc. Events are included even if they are triggered by
However, interactions with the style panel are not included. For a full list of events and sources,
check out the TLUiEventSource and TLUiEventMap types.
It also shows the relevant code snippet for each event. This is useful for debugging and learning
the tldraw SDK.
We can pass a handler function to the onUiEvent prop of the Tldraw component. This handler function
will be called with the name of the event and the data associated with the event. We're going to
display these events in a list on the right side of the screen.

Wyświetl plik

@ -0,0 +1,161 @@
const STYLE_EVENT = {
'tldraw:color': 'DefaultColorStyle',
'tldraw:dash': 'DefaultDashStyle',
'tldraw:fill': 'DefaultFillStyle',
'tldraw:font': 'DefaultFontStyle',
'tldraw:horizontalAlign': 'DefaultHorizontalAlignStyle',
'tldraw:size': 'DefaultSizeStyle',
'tldraw:verticalAlign': 'DefaultVerticalAlignStyle',
'tldraw:geo': 'GeoShapeGeoStyle',
}
const REORDER_EVENT = {
toFront: 'bringToFront',
forward: 'bringForward',
backward: 'sendBackward',
toBack: 'sendToBack',
}
const SHAPES_META_EVENT = {
'group-shapes': 'groupShapes',
'ungroup-shapes': 'ungroupShapes',
'delete-shapes': 'deleteShapes',
}
const SHAPES_EVENT = {
'distribute-shapes': 'distributeShapes',
'align-shapes': 'alignShapes',
'stretch-shapes': 'stretchShapes',
'flip-shapes': 'flipShapes',
}
const USER_PREFS_EVENT = {
'toggle-snap-mode': 'isSnapMode',
'toggle-dark-mode': 'isDarkMode',
'toggle-reduce-motion': 'animationSpeed',
'toggle-edge-scrolling': 'edgeScrollSpeed',
}
const PREFS_EVENT = {
'toggle-transparent': 'exportBackground',
'toggle-tool-lock': 'isToolLocked',
'toggle-focus-mode': 'isFocusMode',
'toggle-grid-mode': 'isGridMode',
'toggle-debug-mode': 'isDebugMode',
}
const ZOOM_EVENT = {
'zoom-in': 'zoomIn',
'zoom-out': 'zoomOut',
'reset-zoom': 'resetZoom',
'zoom-to-fit': 'zoomToFit',
'zoom-to-selection': 'zoomToSelection',
'zoom-to-content': 'zoomToContent',
}
export function getCodeSnippet(name: string, data: any) {
let codeSnippet = ''
if (name === 'set-style') {
if (data.id === 'opacity') {
codeSnippet = `editor.setOpacityForNextShapes(${data.value});`
} else {
codeSnippet = `editor.setStyleForNextShapes(${
STYLE_EVENT[data.id as keyof typeof STYLE_EVENT] ?? '?'
}, '${data.value}');`
}
} else if (['rotate-ccw', 'rotate-cw'].includes(name)) {
codeSnippet = 'editor.rotateShapesBy(editor.getSelectedShapeIds(), <number>)'
} else if (name === 'edit-link') {
codeSnippet =
'editor.updateShapes([{ id: editor.getOnlySelectedShape().id, type: editor.getOnlySelectedShape().type, props: { url: <url> }, }, ])'
} else if (name.startsWith('export-as')) {
codeSnippet = `exportAs(editor.getSelectedShapeIds(), '${data.format}')`
} else if (name.startsWith('copy-as')) {
codeSnippet = `copyAs(editor.getSelectedShapeIds(), '${data.format}')`
} else if (name === 'select-all-shapes') {
codeSnippet = `editor.selectAll()`
} else if (name === 'select-none-shapes') {
codeSnippet = `editor.selectNone()`
} else if (name === 'reorder-shapes') {
codeSnippet = `editor.${
REORDER_EVENT[data.operation as keyof typeof REORDER_EVENT] ?? '?'
}(editor.getSelectedShapeIds())`
} else if (['group-shapes', 'ungroup-shapes', 'delete-shapes'].includes(name)) {
codeSnippet = `editor.${
SHAPES_META_EVENT[name as keyof typeof SHAPES_META_EVENT] ?? '?'
}(editor.getSelectedShapeIds())`
} else if (name === 'stack-shapes') {
codeSnippet = `editor.stackShapes(editor.getSelectedShapeIds(), '${data.operation}', 16)`
} else if (name === 'pack-shapes') {
codeSnippet = `editor.packShapes(editor.getSelectedShapeIds(), 16)`
} else if (name === 'duplicate-shapes') {
codeSnippet = `editor.duplicateShapes(editor.getSelectedShapeIds(), {x: <value>, y: <value>})`
} else if (name.endsWith('-shapes')) {
codeSnippet = `editor.${
SHAPES_EVENT[name as keyof typeof SHAPES_EVENT] ?? '?'
}(editor.getSelectedShapeIds(), '${data.operation}')`
} else if (name === 'select-tool') {
if (data.id === 'media') {
codeSnippet = 'insertMedia()'
} else if (data.id.startsWith('geo-')) {
codeSnippet = `\n editor.updateInstanceState({
stylesForNextShape: {
...editor.getInstanceState().stylesForNextShape,
[GeoShapeGeoStyle.id]: '${data.id.replace('geo-', '')}',
},
}, { ephemeral: true });
editor.setCurrentTool('${data.id}')`
} else {
codeSnippet = `editor.setCurrentTool('${data.id}')`
}
} else if (name === 'print') {
codeSnippet = 'printSelectionOrPages()'
} else if (name === 'unlock-all') {
codeSnippet = `\n const updates = [] as TLShapePartial[]
for (const shape of editor.getCurrentPageShapes()) {
if (shape.isLocked) {
updates.push({ id: shape.id, type: shape.type, isLocked: false })
}
}
if (updates.length > 0) {
editor.updateShapes(updates)
}`
} else if (['undo', 'redo'].includes(name)) {
codeSnippet = `editor.${name}()`
} else if (['cut', 'copy'].includes(name)) {
codeSnippet = `\n const { ${name} } = useMenuClipboardEvents();\n ${name}()`
} else if (name === 'paste') {
codeSnippet = `\n const { paste } = useMenuClipboardEvents();\n navigator.clipboard?.read().then((clipboardItems) => {\n paste(clipboardItems)\n })`
} else if (name === 'stop-following') {
codeSnippet = `editor.stopFollowingUser()`
} else if (name === 'exit-pen-mode') {
codeSnippet = `editor.updateInstanceState({ isPenMode: false })`
} else if (name === 'remove-frame') {
codeSnippet = `removeFrame(editor, editor.getSelectedShapes().map((shape) => shape.id))`
} else if (name === 'fit-frame-to-content') {
codeSnippet = `fitFrameToContent(editor, editor.getOnlySelectedShape().id)`
} else if (name.startsWith('zoom-') || name === 'reset-zoom') {
if (name === 'zoom-to-content') {
codeSnippet = 'editor.zoomToContent()'
} else {
codeSnippet = `editor.${ZOOM_EVENT[name as keyof typeof ZOOM_EVENT]}(${
name !== 'zoom-to-fit' && name !== 'zoom-to-selection'
? 'editor.getViewportScreenCenter(), '
: ''
}{ duration: 320 })`
}
} else if (name.startsWith('toggle-')) {
if (name === 'toggle-lock') {
codeSnippet = `editor.toggleLock(editor.getSelectedShapeIds())`
} else {
const userPrefName = USER_PREFS_EVENT[name as keyof typeof USER_PREFS_EVENT]
const prefName = PREFS_EVENT[name as keyof typeof PREFS_EVENT]
codeSnippet = userPrefName
? `editor.user.updateUserPreferences({ ${userPrefName}: <value> })`
: `editor.updateInstanceState({ ${prefName}: !editor.getInstanceState().${prefName} })`
}
}
return codeSnippet
}

Wyświetl plik

@ -213,8 +213,9 @@ li.examples__sidebar__item {
align-items: stretch;
}
/* ----------------- Footer Buttons ----------------- */
/* ----------------- Header/Footer Buttons ----------------- */
.example__sidebar__header-links,
.example__sidebar__footer-links {
display: flex;
flex-direction: column;
@ -225,6 +226,7 @@ li.examples__sidebar__item {
border-top: 1px solid #e8e8e8;
}
.example__sidebar__header-link,
.example__sidebar__footer-link {
padding: 8px 8px;
border-radius: 6px;
@ -241,7 +243,7 @@ li.examples__sidebar__item {
0px 1px 3px rgba(0, 0, 0, 0.04);
}
.example__sidebar__footer-link > a {
a.example__sidebar__header-link {
color: white;
}

Wyświetl plik

@ -1533,6 +1533,11 @@ export interface TLUiEventMap {
id: string;
};
// (undocumented)
'set-style': {
id: string;
value: number | string;
};
// (undocumented)
'stack-shapes': {
operation: 'horizontal' | 'vertical';
};
@ -1597,7 +1602,7 @@ export interface TLUiEventMap {
}
// @public (undocumented)
export type TLUiEventSource = 'actions-menu' | 'context-menu' | 'debug-panel' | 'dialog' | 'export-menu' | 'help-menu' | 'helper-buttons' | 'kbd' | 'menu' | 'navigation-zone' | 'page-menu' | 'people-menu' | 'quick-actions' | 'share-menu' | 'toolbar' | 'unknown' | 'zoom-menu';
export type TLUiEventSource = 'actions-menu' | 'context-menu' | 'debug-panel' | 'dialog' | 'export-menu' | 'help-menu' | 'helper-buttons' | 'kbd' | 'menu' | 'navigation-zone' | 'page-menu' | 'people-menu' | 'quick-actions' | 'share-menu' | 'style-panel' | 'toolbar' | 'unknown' | 'zoom-menu';
// @public (undocumented)
export type TLUiHelpMenuSchemaContextType = TLUiMenuSchema;

Wyświetl plik

@ -17370,6 +17370,33 @@
"endIndex": 2
}
},
{
"kind": "PropertySignature",
"canonicalReference": "@tldraw/tldraw!TLUiEventMap#\"set-style\":member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "'set-style': "
},
{
"kind": "Content",
"text": "{\n id: string;\n value: number | string;\n }"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "\"set-style\"",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
},
{
"kind": "PropertySignature",
"canonicalReference": "@tldraw/tldraw!TLUiEventMap#\"stack-shapes\":member",
@ -18167,7 +18194,7 @@
},
{
"kind": "Content",
"text": "'actions-menu' | 'context-menu' | 'debug-panel' | 'dialog' | 'export-menu' | 'help-menu' | 'helper-buttons' | 'kbd' | 'menu' | 'navigation-zone' | 'page-menu' | 'people-menu' | 'quick-actions' | 'share-menu' | 'toolbar' | 'unknown' | 'zoom-menu'"
"text": "'actions-menu' | 'context-menu' | 'debug-panel' | 'dialog' | 'export-menu' | 'help-menu' | 'helper-buttons' | 'kbd' | 'menu' | 'navigation-zone' | 'page-menu' | 'people-menu' | 'quick-actions' | 'share-menu' | 'style-panel' | 'toolbar' | 'unknown' | 'zoom-menu'"
},
{
"kind": "Content",

Wyświetl plik

@ -2,7 +2,7 @@ import { track, useEditor } from '@tldraw/editor'
import { useActions } from '../hooks/useActions'
import { Button } from './primitives/Button'
export const StopFollowing = track(function ExitPenMode() {
export const StopFollowing = track(function StopFollowing() {
const editor = useEditor()
const actions = useActions()

Wyświetl plik

@ -17,6 +17,7 @@ import {
useEditor,
} from '@tldraw/editor'
import React, { useCallback } from 'react'
import { useUiEvents } from '../../hooks/useEventsProvider'
import { useRelevantStyles } from '../../hooks/useRevelantStyles'
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { Button } from '../primitives/Button'
@ -73,6 +74,7 @@ export const StylePanel = function StylePanel({ isMobile }: StylePanelProps) {
function useStyleChangeCallback() {
const editor = useEditor()
const trackEvent = useUiEvents()
return React.useMemo(() => {
return function handleStyleChange<T>(style: StyleProp<T>, value: T, squashing: boolean) {
@ -83,8 +85,10 @@ function useStyleChangeCallback() {
editor.setStyleForNextShapes(style, value, { squashing })
editor.updateInstanceState({ isChangingStyle: true })
})
trackEvent('set-style', { source: 'style-panel', id: style.id, value: value as string })
}
}, [editor])
}, [editor, trackEvent])
}
const tldrawSupportedOpacities = [0.1, 0.25, 0.5, 0.75, 1] as const
@ -97,6 +101,7 @@ function CommonStylePickerSet({
opacity: SharedStyle<number>
}) {
const editor = useEditor()
const trackEvent = useUiEvents()
const msg = useTranslation()
const handleValueChange = useStyleChangeCallback()
@ -111,8 +116,10 @@ function CommonStylePickerSet({
editor.setOpacityForNextShapes(item, { ephemeral })
editor.updateInstanceState({ isChangingStyle: true })
})
trackEvent('set-style', { source: 'style-panel', id: 'opacity', value })
},
[editor]
[editor, trackEvent]
)
const color = styles.get(DefaultColorStyle)

Wyświetl plik

@ -18,6 +18,7 @@ export type TLUiEventSource =
| 'dialog'
| 'help-menu'
| 'helper-buttons'
| 'style-panel'
| 'unknown'
/** @public */
@ -72,6 +73,7 @@ export interface TLUiEventMap {
copy: null
paste: null
cut: null
'set-style': { id: string; value: string | number }
'toggle-transparent': null
'toggle-snap-mode': null
'toggle-tool-lock': null

Wyświetl plik

@ -13565,11 +13565,13 @@ __metadata:
"@radix-ui/react-alert-dialog": "npm:^1.0.5"
"@tldraw/assets": "workspace:*"
"@tldraw/tldraw": "workspace:*"
"@types/lodash": "npm:^4.14.188"
"@vercel/analytics": "npm:^1.1.1"
"@vitejs/plugin-react": "npm:^4.2.0"
classnames: "npm:^2.3.2"
dotenv: "npm:^16.3.1"
lazyrepo: "npm:0.0.0-alpha.27"
lodash: "npm:^4.17.21"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
react-router-dom: "npm:^6.17.0"