security: enforce use of our fetch function and its default referrerpolicy (#3884)
followup to https://github.com/tldraw/tldraw/pull/3881 to enforce this in the codebase Describe what your pull request does. If appropriate, add GIFs or images showing the before and after. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [x] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [x] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know
This commit is contained in:
parent
9adb5eec5a
commit
3adae06d9c
68 changed files with 300 additions and 119 deletions
94
.eslintrc.js
94
.eslintrc.js
|
@ -2,6 +2,7 @@ module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
'prettier',
|
'prettier',
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:@next/next/core-web-vitals',
|
'plugin:@next/next/core-web-vitals',
|
||||||
],
|
],
|
||||||
|
@ -12,6 +13,7 @@ module.exports = {
|
||||||
'import',
|
'import',
|
||||||
'local',
|
'local',
|
||||||
'@next/next',
|
'@next/next',
|
||||||
|
'react',
|
||||||
'react-hooks',
|
'react-hooks',
|
||||||
'deprecation',
|
'deprecation',
|
||||||
],
|
],
|
||||||
|
@ -23,15 +25,17 @@ module.exports = {
|
||||||
rules: {
|
rules: {
|
||||||
'deprecation/deprecation': 'error',
|
'deprecation/deprecation': 'error',
|
||||||
'@next/next/no-html-link-for-pages': 'off',
|
'@next/next/no-html-link-for-pages': 'off',
|
||||||
'react/jsx-key': 'off',
|
|
||||||
'no-non-null-assertion': 'off',
|
'no-non-null-assertion': 'off',
|
||||||
'no-fallthrough': 'off',
|
'no-fallthrough': 'off',
|
||||||
|
'react/jsx-no-target-blank': 'error',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
'@typescript-eslint/no-fallthrough': 'off',
|
'@typescript-eslint/no-fallthrough': 'off',
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
'react/display-name': 'off',
|
'react/display-name': 'off',
|
||||||
'@next/next/no-img-element': 'off',
|
'@next/next/no-img-element': 'off',
|
||||||
|
'react/prop-types': 'off',
|
||||||
'@typescript-eslint/no-extra-semi': 'off',
|
'@typescript-eslint/no-extra-semi': 'off',
|
||||||
'no-mixed-spaces-and-tabs': 'off',
|
'no-mixed-spaces-and-tabs': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
@ -86,39 +90,107 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['packages/editor/**/*', 'packages/tldraw/**/*'],
|
files: ['packages/editor/**/*', 'packages/tldraw/**/*', 'packages/utils/**/*'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-restricted-globals': [
|
'no-restricted-globals': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
name: 'setInterval',
|
name: 'fetch',
|
||||||
message: 'Use the timers from @tldraw/util instead.',
|
message: 'Use the fetch from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Image',
|
||||||
|
message: 'Use the Image from @tldraw/util instead.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'setTimeout',
|
name: 'setTimeout',
|
||||||
message: 'Use the timers from @tldraw/util instead.',
|
message: 'Use the timers from editor.timers instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setInterval',
|
||||||
|
message: 'Use the timers from editor.timers instead.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'requestAnimationFrame',
|
name: 'requestAnimationFrame',
|
||||||
message: 'Use the timers from @tldraw/util instead.',
|
message: 'Use the timers from editor.timers instead.',
|
||||||
},
|
},
|
||||||
|
{ name: 'structuredClone', message: 'Use structuredClone from @tldraw/util instead' },
|
||||||
],
|
],
|
||||||
'no-restricted-properties': [
|
'no-restricted-properties': [
|
||||||
'error',
|
'error',
|
||||||
|
{
|
||||||
|
object: 'window',
|
||||||
|
property: 'fetch',
|
||||||
|
message: 'Use the fetch from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
object: 'window',
|
||||||
|
property: 'Image',
|
||||||
|
message: 'Use the Image from @tldraw/util instead.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
object: 'window',
|
object: 'window',
|
||||||
property: 'setTimeout',
|
property: 'setTimeout',
|
||||||
message: 'Use the timers from @tldraw/util instead.',
|
message: 'Use the timers from editor.timers instead.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
object: 'window',
|
object: 'window',
|
||||||
property: 'setInterval',
|
property: 'setInterval',
|
||||||
message: 'Use the timers from @tldraw/util instead.',
|
message: 'Use the timers from editor.timers instead.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
object: 'window',
|
object: 'window',
|
||||||
property: 'requestAnimationFrame',
|
property: 'requestAnimationFrame',
|
||||||
message: 'Use the timers from @tldraw/util instead.',
|
message: 'Use the timers from editor.timers instead.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-restricted-syntax': [
|
||||||
|
'error',
|
||||||
|
{ selector: "MethodDefinition[kind='set']", message: 'Property setters are not allowed' },
|
||||||
|
{ selector: "MethodDefinition[kind='get']", message: 'Property getters are not allowed' },
|
||||||
|
{
|
||||||
|
selector: 'Identifier[name=localStorage]',
|
||||||
|
message: 'Use the getFromLocalStorage/setInLocalStorage helpers instead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'Identifier[name=sessionStorage]',
|
||||||
|
message: 'Use the getFromSessionStorage/setInSessionStorage helpers instead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
"JSXElement[openingElement.name.name='img']:not(:has(JSXAttribute[name.name='referrerPolicy']))",
|
||||||
|
message: 'You must pass `referrerPolicy` when creating an <img>.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// This overrides the default config for the given matching paths.
|
||||||
|
{
|
||||||
|
files: ['apps/dotcom/**/*'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-globals': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
name: 'fetch',
|
||||||
|
message: 'Use the fetch from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Image',
|
||||||
|
message: 'Use the Image from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
{ name: 'structuredClone', message: 'Use structuredClone from @tldraw/util instead' },
|
||||||
|
],
|
||||||
|
'no-restricted-properties': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
object: 'window',
|
||||||
|
property: 'fetch',
|
||||||
|
message: 'Use the fetch from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
object: 'window',
|
||||||
|
property: 'Image',
|
||||||
|
message: 'Use the Image from @tldraw/util instead.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -136,10 +208,12 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['*.test.ts', '*.spec.ts'],
|
files: ['*.test.ts', '*.test.tsx', '*.spec.ts'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-restricted-properties': 'off',
|
'no-restricted-properties': 'off',
|
||||||
'no-restricted-globals': 'off',
|
'no-restricted-globals': 'off',
|
||||||
|
'react/jsx-key': 'off',
|
||||||
|
'react/no-string-refs': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -28,17 +28,17 @@ export default async function ClaPage() {
|
||||||
You reserve all right, title, and interest in and to Your Contributions.{' '}
|
You reserve all right, title, and interest in and to Your Contributions.{' '}
|
||||||
</p>
|
</p>
|
||||||
<p>1. Definitions. </p>
|
<p>1. Definitions. </p>
|
||||||
<p>"You" (or "Your") means the individual identified above. </p>
|
<p>“You” (or “Your”) means the individual identified above. </p>
|
||||||
<p>
|
<p>
|
||||||
"Contribution" means any original work of authorship, including any modifications or
|
“Contribution” means any original work of authorship, including any modifications or
|
||||||
additions to an existing work, that is intentionally submitted by You to tldraw for
|
additions to an existing work, that is intentionally submitted by You to tldraw for
|
||||||
inclusion in, or documentation of, any of the products owned or managed by tldraw (each, a
|
inclusion in, or documentation of, any of the products owned or managed by tldraw (each, a
|
||||||
"Work"). For the purposes of this definition, "submitted" means any form of electronic,
|
“Work”). For the purposes of this definition, “submitted” means any form of electronic,
|
||||||
verbal, or written communication sent to tldraw or its representatives, including but not
|
verbal, or written communication sent to tldraw or its representatives, including but not
|
||||||
limited to communication on electronic mailing lists, source code control systems, and
|
limited to communication on electronic mailing lists, source code control systems, and
|
||||||
issue tracking systems that are managed by, or on behalf of, tldraw for the purpose of
|
issue tracking systems that are managed by, or on behalf of, tldraw for the purpose of
|
||||||
discussing and improving the Works, but excluding communication that is conspicuously
|
discussing and improving the Works, but excluding communication that is conspicuously
|
||||||
marked or otherwise designated in writing by You as "Not a Contribution."{' '}
|
marked or otherwise designated in writing by You as “Not a Contribution.”{' '}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
2. Grant of Copyright License. You hereby grant to tldraw a perpetual, worldwide,
|
2. Grant of Copyright License. You hereby grant to tldraw a perpetual, worldwide,
|
||||||
|
@ -74,7 +74,7 @@ export default async function ClaPage() {
|
||||||
6. You are not expected to provide support for Your Contributions, except to the extent
|
6. You are not expected to provide support for Your Contributions, except to the extent
|
||||||
You desire to provide support. You may provide support for free, for a fee, or not at all.
|
You desire to provide support. You may provide support for free, for a fee, or not at all.
|
||||||
Unless required by applicable law or agreed to in writing, You provide Your Contributions
|
Unless required by applicable law or agreed to in writing, You provide Your Contributions
|
||||||
on an "as is" basis, without warranties or conditions of any kind, either express or
|
on an “as is” basis, without warranties or conditions of any kind, either express or
|
||||||
implied, including, without limitation, any warranties or conditions of title,
|
implied, including, without limitation, any warranties or conditions of title,
|
||||||
non-infringement, merchantability, or fitness for a particular purpose.{' '}
|
non-infringement, merchantability, or fitness for a particular purpose.{' '}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -53,13 +53,13 @@ export default async function LicensePage() {
|
||||||
available upon request and do so only under a license that complies with this License.
|
available upon request and do so only under a license that complies with this License.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
That that the word "tldraw" shall not be used to refer to any derivative works of the
|
That that the word “tldraw” shall not be used to refer to any derivative works of the
|
||||||
Software except in the phrase "Based on the tldraw library (https://tldraw.com)", provided
|
Software except in the phrase “Based on the tldraw library (https://tldraw.com)”, provided
|
||||||
such phrase is not used to promote the derivative works or to imply that tldraw endorses
|
such phrase is not used to promote the derivative works or to imply that tldraw endorses
|
||||||
you or the derivative works.
|
you or the derivative works.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
THAT THE SOFTWARE COMES "AS IS", WITH NO WARRANTIES. THIS MEANS NO EXPRESS, IMPLIED OR
|
THAT THE SOFTWARE COMES “AS IS”, WITH NO WARRANTIES. THIS MEANS NO EXPRESS, IMPLIED OR
|
||||||
STATUTORY WARRANTY, INCLUDING WITHOUT LIMITATION, WARRANTIES OF MERCHANTABILITY OR FITNESS
|
STATUTORY WARRANTY, INCLUDING WITHOUT LIMITATION, WARRANTIES OF MERCHANTABILITY OR FITNESS
|
||||||
FOR A PARTICULAR PURPOSE OR ANY WARRANTY OF TITLE OR NON-INFRINGEMENT. ALSO, YOU MUST PASS
|
FOR A PARTICULAR PURPOSE OR ANY WARRANTY OF TITLE OR NON-INFRINGEMENT. ALSO, YOU MUST PASS
|
||||||
THIS DISCLAIMER ON WHENEVER YOU DISTRIBUTE THE SOFTWARE OR DERIVATIVE WORKS.
|
THIS DISCLAIMER ON WHENEVER YOU DISTRIBUTE THE SOFTWARE OR DERIVATIVE WORKS.
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default async function NotFound() {
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1>Not found.</h1>
|
<h1>Not found.</h1>
|
||||||
</div>
|
</div>
|
||||||
<p>There's nothing here. :(</p>
|
<p>There’s nothing here. :(</p>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,6 +24,7 @@ export function Header({ sectionId }: { sectionId?: string }) {
|
||||||
href="https://x.com/tldraw/"
|
href="https://x.com/tldraw/"
|
||||||
className="sidebar__button icon-button"
|
className="sidebar__button icon-button"
|
||||||
title="twitter"
|
title="twitter"
|
||||||
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Icon icon="twitter" />
|
<Icon icon="twitter" />
|
||||||
|
@ -32,6 +33,7 @@ export function Header({ sectionId }: { sectionId?: string }) {
|
||||||
href="https://discord.com/invite/SBBEVCA4PG"
|
href="https://discord.com/invite/SBBEVCA4PG"
|
||||||
className="sidebar__button icon-button"
|
className="sidebar__button icon-button"
|
||||||
title="discord"
|
title="discord"
|
||||||
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Icon icon="discord" />
|
<Icon icon="discord" />
|
||||||
|
@ -40,6 +42,7 @@ export function Header({ sectionId }: { sectionId?: string }) {
|
||||||
href="https://github.com/tldraw/tldraw"
|
href="https://github.com/tldraw/tldraw"
|
||||||
className="sidebar__button icon-button"
|
className="sidebar__button icon-button"
|
||||||
title="github"
|
title="github"
|
||||||
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Icon icon="github" />
|
<Icon icon="github" />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
||||||
import { RoomSnapshot } from '@tldraw/tlsync'
|
import { RoomSnapshot } from '@tldraw/tlsync'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { Tldraw } from 'tldraw'
|
import { Tldraw, fetch } from 'tldraw'
|
||||||
import '../../../styles/core.css'
|
import '../../../styles/core.css'
|
||||||
import { assetUrls } from '../../utils/assetUrls'
|
import { assetUrls } from '../../utils/assetUrls'
|
||||||
import { useFileSystem } from '../../utils/useFileSystem'
|
import { useFileSystem } from '../../utils/useFileSystem'
|
||||||
|
|
|
@ -62,7 +62,7 @@ export function IFrameProtector({
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor tl-container">
|
<div className="tldraw__editor tl-container">
|
||||||
<div className="iframe-warning__container">
|
<div className="iframe-warning__container">
|
||||||
<a className="iframe-warning__link" href={url} target="_blank">
|
<a className="iframe-warning__link" href={url} rel="noopener noreferrer" target="_blank">
|
||||||
{'Visit this page on tldraw.com'}
|
{'Visit this page on tldraw.com'}
|
||||||
<svg
|
<svg
|
||||||
width="15"
|
width="15"
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
TldrawUiMenuContextProvider,
|
TldrawUiMenuContextProvider,
|
||||||
TldrawUiMenuGroup,
|
TldrawUiMenuGroup,
|
||||||
TldrawUiMenuItem,
|
TldrawUiMenuItem,
|
||||||
|
fetch,
|
||||||
unwrapLabel,
|
unwrapLabel,
|
||||||
useActions,
|
useActions,
|
||||||
useContainer,
|
useContainer,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
MediaHelpers,
|
MediaHelpers,
|
||||||
TLAsset,
|
TLAsset,
|
||||||
TLAssetId,
|
TLAssetId,
|
||||||
|
fetch,
|
||||||
getHashForString,
|
getHashForString,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
||||||
import { RoomSnapshot } from '@tldraw/tlsync'
|
import { RoomSnapshot } from '@tldraw/tlsync'
|
||||||
|
import { fetch } from 'tldraw'
|
||||||
import '../../styles/globals.css'
|
import '../../styles/globals.css'
|
||||||
import { BoardHistorySnapshot } from '../components/BoardHistorySnapshot/BoardHistorySnapshot'
|
import { BoardHistorySnapshot } from '../components/BoardHistorySnapshot/BoardHistorySnapshot'
|
||||||
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
|
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
||||||
|
import { fetch } from 'tldraw'
|
||||||
import { BoardHistoryLog } from '../components/BoardHistoryLog/BoardHistoryLog'
|
import { BoardHistoryLog } from '../components/BoardHistoryLog/BoardHistoryLog'
|
||||||
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
|
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
|
||||||
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
|
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SerializedSchema, TLRecord } from 'tldraw'
|
import { SerializedSchema, TLRecord, fetch } from 'tldraw'
|
||||||
import '../../styles/globals.css'
|
import '../../styles/globals.css'
|
||||||
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
|
import { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
|
||||||
import { SnapshotsEditor } from '../components/SnapshotsEditor'
|
import { SnapshotsEditor } from '../components/SnapshotsEditor'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { CreateRoomRequestBody, ROOM_PREFIX, Snapshot } from '@tldraw/dotcom-sha
|
||||||
import { schema } from '@tldraw/tlsync'
|
import { schema } from '@tldraw/tlsync'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
import { TldrawUiButton } from 'tldraw'
|
import { TldrawUiButton, fetch } from 'tldraw'
|
||||||
import '../../styles/globals.css'
|
import '../../styles/globals.css'
|
||||||
import { getParentOrigin } from '../utils/iFrame'
|
import { getParentOrigin } from '../utils/iFrame'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { getAssetUrlsByImport } from '@tldraw/assets/imports.vite'
|
import { getAssetUrlsByImport } from '@tldraw/assets/imports.vite'
|
||||||
|
import { Image } from 'tldraw'
|
||||||
|
|
||||||
export const assetUrls = getAssetUrlsByImport()
|
export const assetUrls = getAssetUrlsByImport()
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@ async function preloadIcons() {
|
||||||
|
|
||||||
function preloadIcon(url: string) {
|
function preloadIcon(url: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const image = new Image()
|
const image = Image()
|
||||||
// this isn't known by typescript but it works
|
// this isn't known by typescript but it works
|
||||||
;(image as any).fetchPriority = 'low'
|
;(image as any).fetchPriority = 'low'
|
||||||
image.onload = resolve
|
image.onload = resolve
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TLAsset } from 'tldraw'
|
import { TLAsset, fetch } from 'tldraw'
|
||||||
|
|
||||||
export async function cloneAssetForShare(
|
export async function cloneAssetForShare(
|
||||||
asset: TLAsset,
|
asset: TLAsset,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
MediaHelpers,
|
MediaHelpers,
|
||||||
TLAsset,
|
TLAsset,
|
||||||
TLAssetId,
|
TLAssetId,
|
||||||
|
fetch,
|
||||||
getHashForString,
|
getHashForString,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
|
@ -18,7 +19,6 @@ export async function createAssetFromFile({ file }: { type: 'file'; file: File }
|
||||||
await fetch(url, {
|
await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: file,
|
body: file,
|
||||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
|
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AssetRecordType, TLAsset, getHashForString } from 'tldraw'
|
import { AssetRecordType, TLAsset, fetch, getHashForString } from 'tldraw'
|
||||||
import { BOOKMARK_ENDPOINT } from './config'
|
import { BOOKMARK_ENDPOINT } from './config'
|
||||||
|
|
||||||
interface ResponseBody {
|
interface ResponseBody {
|
||||||
|
@ -45,7 +45,6 @@ export async function createAssetFromUrl({ url }: { type: 'url'; url: string }):
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
mode: 'no-cors',
|
mode: 'no-cors',
|
||||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
||||||
})
|
})
|
||||||
const html = await resp.text()
|
const html = await resp.text()
|
||||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { openDB } from 'idb'
|
import { openDB } from 'idb'
|
||||||
import { Editor, TLV1Document, buildFromV1Document } from 'tldraw'
|
import { Editor, TLV1Document, buildFromV1Document, fetch } from 'tldraw'
|
||||||
|
|
||||||
export function isEditorEmpty(editor: Editor) {
|
export function isEditorEmpty(editor: Editor) {
|
||||||
const hasAnyShapes = editor.store.allRecords().some((r) => r.typeName === 'shape')
|
const hasAnyShapes = editor.store.allRecords().some((r) => r.typeName === 'shape')
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
TLUiOverrides,
|
TLUiOverrides,
|
||||||
TLUiToastsContextType,
|
TLUiToastsContextType,
|
||||||
TLUiTranslationKey,
|
TLUiTranslationKey,
|
||||||
|
fetch,
|
||||||
isShape,
|
isShape,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
import { useMultiplayerAssets } from '../hooks/useMultiplayerAssets'
|
import { useMultiplayerAssets } from '../hooks/useMultiplayerAssets'
|
||||||
|
|
|
@ -45,6 +45,7 @@ export function ExamplePage({
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://twitter.com/tldraw"
|
href="https://twitter.com/tldraw"
|
||||||
|
rel="noopener noreferrer"
|
||||||
title="twitter"
|
title="twitter"
|
||||||
className="hoverable"
|
className="hoverable"
|
||||||
>
|
>
|
||||||
|
@ -53,6 +54,7 @@ export function ExamplePage({
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/tldraw/tldraw"
|
href="https://github.com/tldraw/tldraw"
|
||||||
|
rel="noopener noreferrer"
|
||||||
title="github"
|
title="github"
|
||||||
className="hoverable"
|
className="hoverable"
|
||||||
>
|
>
|
||||||
|
@ -61,6 +63,7 @@ export function ExamplePage({
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://discord.com/invite/SBBEVCA4PG"
|
href="https://discord.com/invite/SBBEVCA4PG"
|
||||||
|
rel="noopener noreferrer"
|
||||||
title="discord"
|
title="discord"
|
||||||
className="hoverable"
|
className="hoverable"
|
||||||
>
|
>
|
||||||
|
@ -104,6 +107,7 @@ export function ExamplePage({
|
||||||
<a
|
<a
|
||||||
className="example__sidebar__footer-link example__sidebar__footer-link--grey"
|
className="example__sidebar__footer-link example__sidebar__footer-link--grey"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="https://github.com/tldraw/tldraw/issues/new?assignees=&labels=Example%20Request&projects=&template=example_request.yml&title=%5BExample Request%5D%3A+"
|
href="https://github.com/tldraw/tldraw/issues/new?assignees=&labels=Example%20Request&projects=&template=example_request.yml&title=%5BExample Request%5D%3A+"
|
||||||
>
|
>
|
||||||
Request an example
|
Request an example
|
||||||
|
@ -111,6 +115,7 @@ export function ExamplePage({
|
||||||
<a
|
<a
|
||||||
className="example__sidebar__footer-link example__sidebar__footer-link--grey"
|
className="example__sidebar__footer-link example__sidebar__footer-link--grey"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="https://tldraw.dev"
|
href="https://tldraw.dev"
|
||||||
>
|
>
|
||||||
Visit the docs
|
Visit the docs
|
||||||
|
|
|
@ -42,10 +42,10 @@ function ControlledFocusExample() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
The checkbox controls the editor's <code>instanceState.isFocused</code> property.
|
The checkbox controls the editor’s <code>instanceState.isFocused</code> property.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
When the editor is "focused", its keyboard shortcuts will work. When it is not focused, the
|
When the editor is “focused”, its keyboard shortcuts will work. When it is not focused, the
|
||||||
keyboard shortcuts will not work.
|
keyboard shortcuts will not work.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ width: 800, maxWidth: '100%', height: 500 }}>
|
<div style={{ width: 800, maxWidth: '100%', height: 500 }}>
|
||||||
|
@ -67,7 +67,7 @@ function FreeFocusExample() {
|
||||||
<div>
|
<div>
|
||||||
<h2>Free Focus</h2>
|
<h2>Free Focus</h2>
|
||||||
<p>
|
<p>
|
||||||
You can use `onBlur` and `onFocus` to control the editor's focus so that it behaves like a
|
You can use `onBlur` and `onFocus` to control the editor’s focus so that it behaves like a
|
||||||
native form input.
|
native form input.
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { throttle } from 'lodash'
|
||||||
import { useLayoutEffect, useState } from 'react'
|
import { useLayoutEffect, useState } from 'react'
|
||||||
import { Tldraw, createTLStore, getSnapshot, loadSnapshot, throttle } from 'tldraw'
|
import { Tldraw, createTLStore, getSnapshot, loadSnapshot } from 'tldraw'
|
||||||
import 'tldraw/tldraw.css'
|
import 'tldraw/tldraw.css'
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
// There's a guide at the bottom of this file!
|
||||||
|
|
|
@ -155,13 +155,13 @@ function ABunchOfText() {
|
||||||
The fluorescent lights flickered overhead as John sat hunched over his desk, his fingers
|
The fluorescent lights flickered overhead as John sat hunched over his desk, his fingers
|
||||||
tapping rhythmically on the keyboard. He was a software developer, and tonight, he had a
|
tapping rhythmically on the keyboard. He was a software developer, and tonight, he had a
|
||||||
peculiar mission. A mission that would take him deep into the labyrinthine world of web
|
peculiar mission. A mission that would take him deep into the labyrinthine world of web
|
||||||
development. John had stumbled upon a new whiteboard library called "tldraw," a seemingly
|
development. John had stumbled upon a new whiteboard library called “tldraw”, a seemingly
|
||||||
simple tool that promised to revolutionize collaborative drawing on the web. Little did he
|
simple tool that promised to revolutionize collaborative drawing on the web. Little did he
|
||||||
know that this discovery would set off a chain of events that would challenge his skills,
|
know that this discovery would set off a chain of events that would challenge his skills,
|
||||||
test his perseverance, and blur the line between reality and imagination.
|
test his perseverance, and blur the line between reality and imagination.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
With a newfound sense of excitement, John began integrating "tldraw" into his latest
|
With a newfound sense of excitement, John began integrating “tldraw” into his latest
|
||||||
project. As lines of code danced across his screen, he imagined the possibilities that lay
|
project. As lines of code danced across his screen, he imagined the possibilities that lay
|
||||||
ahead. The potential to create virtual spaces where ideas could be shared, concepts could be
|
ahead. The potential to create virtual spaces where ideas could be shared, concepts could be
|
||||||
visualized, and teams could collaborate seamlessly from different corners of the world. It
|
visualized, and teams could collaborate seamlessly from different corners of the world. It
|
||||||
|
@ -169,7 +169,7 @@ function ABunchOfText() {
|
||||||
merged into a harmonious symphony.
|
merged into a harmonious symphony.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
As the night wore on, John's mind became consumed with the whiteboard library. He couldn't
|
As the night wore on, John’s mind became consumed with the whiteboard library. He couldn’t
|
||||||
help but marvel at its elegance and simplicity. With each stroke of his keyboard, he felt a
|
help but marvel at its elegance and simplicity. With each stroke of his keyboard, he felt a
|
||||||
surge of inspiration, a connection to something greater than himself. It was as if the lines
|
surge of inspiration, a connection to something greater than himself. It was as if the lines
|
||||||
of code he was writing were transforming into a digital canvas, waiting to be filled with
|
of code he was writing were transforming into a digital canvas, waiting to be filled with
|
||||||
|
@ -179,7 +179,7 @@ function ABunchOfText() {
|
||||||
take shape.
|
take shape.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Little did John know, this integration of "tldraw" was only the beginning. It would lead him
|
Little did John know, this integration of “tldraw” was only the beginning. It would lead him
|
||||||
down a path filled with unforeseen challenges, where he would confront his own limitations
|
down a path filled with unforeseen challenges, where he would confront his own limitations
|
||||||
and question the very nature of creation. The journey ahead would test his resolve, pushing
|
and question the very nature of creation. The journey ahead would test his resolve, pushing
|
||||||
him to the edge of his sanity. And as he embarked on this perilous adventure, he could not
|
him to the edge of his sanity. And as he embarked on this perilous adventure, he could not
|
||||||
|
|
|
@ -75,7 +75,7 @@ const MyComponentInFront = track(() => {
|
||||||
boxShadow: '0 0 0 1px rgba(0,0,0,0.1), 0 4px 8px rgba(0,0,0,0.1)',
|
boxShadow: '0 0 0 1px rgba(0,0,0,0.1), 0 4px 8px rgba(0,0,0,0.1)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p>This won't scale with zoom.</p>
|
<p>This won’t scale with zoom.</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export default function YjsExample() {
|
export default function YjsExample() {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
We've moved! See{' '}
|
We’ve moved! See{' '}
|
||||||
<a href="https://github.com/tldraw/tldraw-yjs-example">
|
<a href="https://github.com/tldraw/tldraw-yjs-example">
|
||||||
https://github.com/tldraw/tldraw-yjs-example
|
https://github.com/tldraw/tldraw-yjs-example
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AssetRecordType, TLAsset, TLExternalAssetContent, getHashForString } from 'tldraw'
|
import { AssetRecordType, TLAsset, TLExternalAssetContent, fetch, getHashForString } from 'tldraw'
|
||||||
import { rpc } from './rpc'
|
import { rpc } from './rpc'
|
||||||
|
|
||||||
export async function onCreateAssetFromUrl({
|
export async function onCreateAssetFromUrl({
|
||||||
|
@ -30,7 +30,6 @@ export async function onCreateAssetFromUrl({
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
mode: 'no-cors',
|
mode: 'no-cors',
|
||||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
||||||
})
|
})
|
||||||
const html = await resp.text()
|
const html = await resp.text()
|
||||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||||
|
|
|
@ -1073,6 +1073,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
};
|
};
|
||||||
interrupt(): this;
|
interrupt(): this;
|
||||||
isAncestorSelected(shape: TLShape | TLShapeId): boolean;
|
isAncestorSelected(shape: TLShape | TLShapeId): boolean;
|
||||||
|
isDisposed: boolean;
|
||||||
isIn(path: string): boolean;
|
isIn(path: string): boolean;
|
||||||
isInAny(...paths: string[]): boolean;
|
isInAny(...paths: string[]): boolean;
|
||||||
isPointInShape(shape: TLShape | TLShapeId, point: VecLike, opts?: {
|
isPointInShape(shape: TLShape | TLShapeId, point: VecLike, opts?: {
|
||||||
|
|
|
@ -57,8 +57,6 @@
|
||||||
"eventemitter3": "^4.0.7",
|
"eventemitter3": "^4.0.7",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"is-plain-object": "^5.0.0",
|
"is-plain-object": "^5.0.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
|
||||||
"lodash.uniq": "^4.5.0",
|
|
||||||
"nanoid": "4.0.2"
|
"nanoid": "4.0.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -70,8 +68,6 @@
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@types/benchmark": "^2.1.2",
|
"@types/benchmark": "^2.1.2",
|
||||||
"@types/lodash.throttle": "^4.1.7",
|
|
||||||
"@types/lodash.uniq": "^4.5.7",
|
|
||||||
"@types/react-test-renderer": "^18.0.0",
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
"@types/wicg-file-system-access": "^2020.9.5",
|
"@types/wicg-file-system-access": "^2020.9.5",
|
||||||
"benchmark": "^2.1.4",
|
"benchmark": "^2.1.4",
|
||||||
|
|
|
@ -596,6 +596,7 @@ function DebugSvgCopy({ id }: { id: TLShapeId }) {
|
||||||
src={image.src}
|
src={image.src}
|
||||||
width={image.bounds.width}
|
width={image.bounds.width}
|
||||||
height={image.bounds.height}
|
height={image.bounds.height}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|
|
@ -230,6 +230,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
this.options = { ...defaultTldrawOptions, ...options }
|
this.options = { ...defaultTldrawOptions, ...options }
|
||||||
this.store = store
|
this.store = store
|
||||||
|
this.disposables.add(this.store.dispose.bind(this.store))
|
||||||
this.history = new HistoryManager<TLRecord>({
|
this.history = new HistoryManager<TLRecord>({
|
||||||
store,
|
store,
|
||||||
annotateError: (error) => {
|
annotateError: (error) => {
|
||||||
|
@ -718,6 +719,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
*/
|
*/
|
||||||
readonly disposables = new Set<() => void>()
|
readonly disposables = new Set<() => void>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the editor is disposed.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
isDisposed = false
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
private readonly _tickManager
|
private readonly _tickManager
|
||||||
|
|
||||||
|
@ -798,6 +806,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
dispose() {
|
dispose() {
|
||||||
this.disposables.forEach((dispose) => dispose())
|
this.disposables.forEach((dispose) => dispose())
|
||||||
this.disposables.clear()
|
this.disposables.clear()
|
||||||
|
this.isDisposed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------- Shape Utils ------------------ */
|
/* ------------------- Shape Utils ------------------ */
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import throttle from 'lodash.throttle'
|
import { throttle } from '@tldraw/utils'
|
||||||
import { useLayoutEffect } from 'react'
|
import { useLayoutEffect } from 'react'
|
||||||
import { Box } from '../primitives/Box'
|
import { Box } from '../primitives/Box'
|
||||||
import { useEditor } from './useEditor'
|
import { useEditor } from './useEditor'
|
||||||
|
@ -62,6 +62,7 @@ export function useScreenBounds(ref: React.RefObject<HTMLElement>) {
|
||||||
window.removeEventListener('resize', updateBounds)
|
window.removeEventListener('resize', updateBounds)
|
||||||
resizeObserver.disconnect()
|
resizeObserver.disconnect()
|
||||||
scrollingParent?.removeEventListener('scroll', updateBounds)
|
scrollingParent?.removeEventListener('scroll', updateBounds)
|
||||||
|
updateBounds.cancel()
|
||||||
}
|
}
|
||||||
}, [editor, ref])
|
}, [editor, ref])
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ export function useZoomCss() {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
scheduler.detach()
|
scheduler.detach()
|
||||||
|
setScaleDebounced.cancel()
|
||||||
}
|
}
|
||||||
}, [editor, container])
|
}, [editor, container])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { fetch } from '@tldraw/utils'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function dataUrlToFile(url: string, filename: string, mimeType: string) {
|
export function dataUrlToFile(url: string, filename: string, mimeType: string) {
|
||||||
return fetch(url)
|
return fetch(url)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import _uniq from 'lodash.uniq'
|
import { uniq as _uniq } from '@tldraw/utils'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function uniq<T>(
|
export function uniq<T>(
|
||||||
|
|
|
@ -348,6 +348,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
createComputedCache: <Result, Record extends R = R>(name: string, derive: (record: Record) => Result | undefined, isEqual?: ((a: Record, b: Record) => boolean) | undefined) => ComputedCache<Result, Record>;
|
createComputedCache: <Result, Record extends R = R>(name: string, derive: (record: Record) => Result | undefined, isEqual?: ((a: Record, b: Record) => boolean) | undefined) => ComputedCache<Result, Record>;
|
||||||
createSelectedComputedCache: <Selection, Result, Record extends R = R>(name: string, selector: (record: Record) => Selection | undefined, derive: (input: Selection) => Result | undefined) => ComputedCache<Result, Record>;
|
createSelectedComputedCache: <Selection, Result, Record extends R = R>(name: string, selector: (record: Record) => Selection | undefined, derive: (input: Selection) => Result | undefined) => ComputedCache<Result, Record>;
|
||||||
|
// (undocumented)
|
||||||
|
dispose(): void;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
ensureStoreIsUsable(): void;
|
ensureStoreIsUsable(): void;
|
||||||
extractingChanges(fn: () => void): RecordsDiff<R>;
|
extractingChanges(fn: () => void): RecordsDiff<R>;
|
||||||
|
|
|
@ -162,6 +162,15 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
*/
|
*/
|
||||||
private historyReactor: Reactor
|
private historyReactor: Reactor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to dispose of any in-flight timeouts.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private cancelHistoryReactor: () => void = () => {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
|
||||||
readonly schema: StoreSchema<R, Props>
|
readonly schema: StoreSchema<R, Props>
|
||||||
|
|
||||||
readonly props: Props
|
readonly props: Props
|
||||||
|
@ -209,7 +218,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
// If we have accumulated history, flush it and update listeners
|
// If we have accumulated history, flush it and update listeners
|
||||||
this._flushHistory()
|
this._flushHistory()
|
||||||
},
|
},
|
||||||
{ scheduleEffect: (cb) => throttleToNextFrame(cb) }
|
{ scheduleEffect: (cb) => (this.cancelHistoryReactor = throttleToNextFrame(cb)) }
|
||||||
)
|
)
|
||||||
this.scopedTypes = {
|
this.scopedTypes = {
|
||||||
document: new Set(
|
document: new Set(
|
||||||
|
@ -264,6 +273,10 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.cancelHistoryReactor()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters out non-document changes from a diff. Returns null if there are no changes left.
|
* Filters out non-document changes from a diff. Returns null if there are no changes left.
|
||||||
* @param change - the records diff
|
* @param change - the records diff
|
||||||
|
|
|
@ -186,7 +186,13 @@ export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setContainer} style={{ position: 'relative', width: '100%', height: '100%' }}>
|
<div ref={setContainer} style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||||
{url && <img src={url} style={{ width: '100%', height: '100%' }} />}
|
{url && (
|
||||||
|
<img
|
||||||
|
src={url}
|
||||||
|
referrerPolicy="strict-origin-when-cross-origin"
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
assert,
|
assert,
|
||||||
compact,
|
compact,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
|
fetch,
|
||||||
getHashForBuffer,
|
getHashForBuffer,
|
||||||
getHashForString,
|
getHashForString,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
@ -116,7 +117,6 @@ export function registerDefaultExternalContentHandlers(
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
mode: 'no-cors',
|
mode: 'no-cors',
|
||||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
||||||
})
|
})
|
||||||
const html = await resp.text()
|
const html = await resp.text()
|
||||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||||
|
|
|
@ -220,6 +220,8 @@ function updateBookmarkAssetOnUrlChange(editor: Editor, shape: TLBookmarkShape)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createBookmarkAssetOnUrlChange = debounce(async (editor: Editor, shape: TLBookmarkShape) => {
|
const createBookmarkAssetOnUrlChange = debounce(async (editor: Editor, shape: TLBookmarkShape) => {
|
||||||
|
if (editor.isDisposed) return
|
||||||
|
|
||||||
const { url } = shape.props
|
const { url } = shape.props
|
||||||
|
|
||||||
// Create the asset using the external content manager's createAssetFromUrl method.
|
// Create the asset using the external content manager's createAssetFromUrl method.
|
||||||
|
|
|
@ -3,11 +3,13 @@ import {
|
||||||
BaseBoxShapeUtil,
|
BaseBoxShapeUtil,
|
||||||
FileHelpers,
|
FileHelpers,
|
||||||
HTMLContainer,
|
HTMLContainer,
|
||||||
|
Image,
|
||||||
MediaHelpers,
|
MediaHelpers,
|
||||||
TLImageShape,
|
TLImageShape,
|
||||||
TLOnDoubleClickHandler,
|
TLOnDoubleClickHandler,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
Vec,
|
Vec,
|
||||||
|
fetch,
|
||||||
imageShapeMigrations,
|
imageShapeMigrations,
|
||||||
imageShapeProps,
|
imageShapeProps,
|
||||||
structuredClone,
|
structuredClone,
|
||||||
|
@ -19,7 +21,7 @@ import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||||
import { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion'
|
import { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion'
|
||||||
|
|
||||||
async function getDataURIFromURL(url: string): Promise<string> {
|
async function getDataURIFromURL(url: string): Promise<string> {
|
||||||
const response = await fetch(url, { referrerPolicy: 'strict-origin-when-cross-origin' })
|
const response = await fetch(url)
|
||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
return FileHelpers.blobToDataUrl(blob)
|
return FileHelpers.blobToDataUrl(blob)
|
||||||
}
|
}
|
||||||
|
@ -70,7 +72,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
||||||
const url = asset.props.src
|
const url = asset.props.src
|
||||||
if (!url) return
|
if (!url) return
|
||||||
|
|
||||||
const image = new Image()
|
const image = Image()
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
TLDefaultFontStyle,
|
TLDefaultFontStyle,
|
||||||
TLShapeUtilCanvasSvgDef,
|
TLShapeUtilCanvasSvgDef,
|
||||||
debugFlags,
|
debugFlags,
|
||||||
|
fetch,
|
||||||
last,
|
last,
|
||||||
useEditor,
|
useEditor,
|
||||||
useValue,
|
useValue,
|
||||||
|
@ -28,9 +29,7 @@ export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef
|
||||||
const fontFaceRule: string = (font as any).$$_fontface
|
const fontFaceRule: string = (font as any).$$_fontface
|
||||||
if (!url || !fontFaceRule) return null
|
if (!url || !fontFaceRule) return null
|
||||||
|
|
||||||
const fontFile = await (
|
const fontFile = await (await fetch(url)).blob()
|
||||||
await fetch(url, { referrerPolicy: 'strict-origin-when-cross-origin' })
|
|
||||||
).blob()
|
|
||||||
const base64FontFile = await FileHelpers.blobToDataUrl(fontFile)
|
const base64FontFile = await FileHelpers.blobToDataUrl(fontFile)
|
||||||
|
|
||||||
const newFontFaceRule = fontFaceRule.replace(url, base64FontFile)
|
const newFontFaceRule = fontFaceRule.replace(url, base64FontFile)
|
||||||
|
|
|
@ -40,7 +40,6 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function
|
||||||
spellCheck="true"
|
spellCheck="true"
|
||||||
wrap="off"
|
wrap="off"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
datatype="wysiwyg"
|
|
||||||
defaultValue={text}
|
defaultValue={text}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|
|
@ -21,6 +21,10 @@ export class Idle extends StateNode {
|
||||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onExit = () => {
|
||||||
|
updateHoveredShapeId.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
|
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
|
||||||
if (info.key === 'Enter') {
|
if (info.key === 'Enter') {
|
||||||
if (this.editor.getInstanceState().isReadonly) return null
|
if (this.editor.getInstanceState().isReadonly) return null
|
||||||
|
|
|
@ -23,6 +23,8 @@ export class EditingShape extends StateNode {
|
||||||
// Clear the editing shape
|
// Clear the editing shape
|
||||||
this.editor.setEditingShape(null)
|
this.editor.setEditingShape(null)
|
||||||
|
|
||||||
|
updateHoveredShapeId.cancel()
|
||||||
|
|
||||||
const shape = this.editor.getShape(editingShapeId)!
|
const shape = this.editor.getShape(editingShapeId)!
|
||||||
const util = this.editor.getShapeUtil(shape)
|
const util = this.editor.getShapeUtil(shape)
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,10 @@ export class Idle extends StateNode {
|
||||||
this.editor.setCursor({ type: 'default', rotation: 0 })
|
this.editor.setCursor({ type: 'default', rotation: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onExit = () => {
|
||||||
|
updateHoveredShapeId.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||||
updateHoveredShapeId(this.editor)
|
updateHoveredShapeId(this.editor)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,5 +32,7 @@ function _updateHoveredShapeId(editor: Editor) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const updateHoveredShapeId =
|
export const updateHoveredShapeId = throttle(
|
||||||
process.env.NODE_ENV === 'test' ? _updateHoveredShapeId : throttle(_updateHoveredShapeId, 32)
|
_updateHoveredShapeId,
|
||||||
|
process.env.NODE_ENV === 'test' ? 0 : 32
|
||||||
|
)
|
||||||
|
|
|
@ -240,7 +240,7 @@ export function ExampleDialog({
|
||||||
style={{ marginRight: 'auto' }}
|
style={{ marginRight: 'auto' }}
|
||||||
>
|
>
|
||||||
<TldrawUiButtonCheck checked={dontShowAgain} />
|
<TldrawUiButtonCheck checked={dontShowAgain} />
|
||||||
<TldrawUiButtonLabel>Don't show again</TldrawUiButtonLabel>
|
<TldrawUiButtonLabel>Don’t show again</TldrawUiButtonLabel>
|
||||||
</TldrawUiButton>
|
</TldrawUiButton>
|
||||||
)}
|
)}
|
||||||
<TldrawUiButton type="normal" onClick={onCancel}>
|
<TldrawUiButton type="normal" onClick={onCancel}>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Image } from '@tldraw/editor'
|
||||||
import { createContext, useContext, useEffect } from 'react'
|
import { createContext, useContext, useEffect } from 'react'
|
||||||
import { TLUiAssetUrls } from '../assetUrls'
|
import { TLUiAssetUrls } from '../assetUrls'
|
||||||
|
|
||||||
|
@ -16,13 +17,13 @@ export function AssetUrlsProvider({
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const src of Object.values(assetUrls.icons)) {
|
for (const src of Object.values(assetUrls.icons)) {
|
||||||
const image = new Image()
|
const image = Image()
|
||||||
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||||
image.src = src
|
image.src = src
|
||||||
image.decode()
|
image.decode()
|
||||||
}
|
}
|
||||||
for (const src of Object.values(assetUrls.embedIcons)) {
|
for (const src of Object.values(assetUrls.embedIcons)) {
|
||||||
const image = new Image()
|
const image = Image()
|
||||||
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||||
image.src = src
|
image.src = src
|
||||||
image.decode()
|
image.decode()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Editor, TLExternalContentSource, VecLike } from '@tldraw/editor'
|
import { Editor, TLExternalContentSource, VecLike, fetch } from '@tldraw/editor'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the clipboard has a file, create an image shape from the file and paste it into the scene
|
* When the clipboard has a file, create an image shape from the file and paste it into the scene
|
||||||
|
@ -14,12 +14,7 @@ export async function pasteFiles(
|
||||||
point?: VecLike,
|
point?: VecLike,
|
||||||
sources?: TLExternalContentSource[]
|
sources?: TLExternalContentSource[]
|
||||||
) {
|
) {
|
||||||
const blobs = await Promise.all(
|
const blobs = await Promise.all(urls.map(async (url) => await (await fetch(url)).blob()))
|
||||||
urls.map(
|
|
||||||
async (url) =>
|
|
||||||
await (await fetch(url, { referrerPolicy: 'strict-origin-when-cross-origin' })).blob()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
const files = blobs.map((blob) => new File([blob], 'tldrawFile', { type: blob.type }))
|
const files = blobs.map((blob) => new File([blob], 'tldrawFile', { type: blob.type }))
|
||||||
|
|
||||||
editor.mark('paste')
|
editor.mark('paste')
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Editor, TLExternalContentSource, VecLike } from '@tldraw/editor'
|
import { Editor, TLExternalContentSource, VecLike, fetch } from '@tldraw/editor'
|
||||||
import { pasteFiles } from './pasteFiles'
|
import { pasteFiles } from './pasteFiles'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +22,6 @@ export async function pasteUrl(
|
||||||
if (new URL(url).pathname.match(/\.(png|jpe?g|gif|svg|webp)$/i)) {
|
if (new URL(url).pathname.match(/\.(png|jpe?g|gif|svg|webp)$/i)) {
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
||||||
})
|
})
|
||||||
if (resp.headers.get('content-type')?.match(/^image\//)) {
|
if (resp.headers.get('content-type')?.match(/^image\//)) {
|
||||||
editor.mark('paste')
|
editor.mark('paste')
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { LANGUAGES } from '@tldraw/editor'
|
import { LANGUAGES, fetch } from '@tldraw/editor'
|
||||||
import { TLUiAssetUrls } from '../../assetUrls'
|
import { TLUiAssetUrls } from '../../assetUrls'
|
||||||
import { TLUiTranslationKey } from './TLUiTranslationKey'
|
import { TLUiTranslationKey } from './TLUiTranslationKey'
|
||||||
import { DEFAULT_TRANSLATION } from './defaultTranslation'
|
import { DEFAULT_TRANSLATION } from './defaultTranslation'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
Editor,
|
Editor,
|
||||||
|
Image,
|
||||||
PngHelpers,
|
PngHelpers,
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
TLSvgOptions,
|
TLSvgOptions,
|
||||||
|
@ -34,7 +35,7 @@ export async function getSvgAsImage(
|
||||||
const svgUrl = URL.createObjectURL(new Blob([svgString], { type: 'image/svg+xml' }))
|
const svgUrl = URL.createObjectURL(new Blob([svgString], { type: 'image/svg+xml' }))
|
||||||
|
|
||||||
const canvas = await new Promise<HTMLCanvasElement | null>((resolve) => {
|
const canvas = await new Promise<HTMLCanvasElement | null>((resolve) => {
|
||||||
const image = new Image()
|
const image = Image()
|
||||||
image.crossOrigin = 'anonymous'
|
image.crossOrigin = 'anonymous'
|
||||||
|
|
||||||
image.onload = async () => {
|
image.onload = async () => {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
VecModel,
|
VecModel,
|
||||||
clamp,
|
clamp,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
|
fetch,
|
||||||
structuredClone,
|
structuredClone,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { getArrowBindings } from '../../shapes/arrow/shared'
|
import { getArrowBindings } from '../../shapes/arrow/shared'
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
UnknownRecord,
|
UnknownRecord,
|
||||||
createTLStore,
|
createTLStore,
|
||||||
exhaustiveSwitchError,
|
exhaustiveSwitchError,
|
||||||
|
fetch,
|
||||||
partition,
|
partition,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { TLUiToastsContextType } from '../../ui/context/toasts'
|
import { TLUiToastsContextType } from '../../ui/context/toasts'
|
||||||
|
@ -188,9 +189,7 @@ export async function serializeTldrawJson(store: TLStore): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// try to save the asset as a base64 string
|
// try to save the asset as a base64 string
|
||||||
assetSrcToSave = await FileHelpers.blobToDataUrl(
|
assetSrcToSave = await FileHelpers.blobToDataUrl(
|
||||||
await (
|
await (await fetch(record.props.src)).blob()
|
||||||
await fetch(record.props.src, { referrerPolicy: 'strict-origin-when-cross-origin' })
|
|
||||||
).blob()
|
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
// if that fails, just save the original src
|
// if that fails, just save the original src
|
||||||
|
|
|
@ -417,6 +417,8 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
|
||||||
close() {
|
close() {
|
||||||
this.debug('closing')
|
this.debug('closing')
|
||||||
this.disposables.forEach((dispose) => dispose())
|
this.disposables.forEach((dispose) => dispose())
|
||||||
|
this.flushPendingPushRequests.cancel?.()
|
||||||
|
this.scheduleRebase.cancel?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
lastPushedPresenceState: R | null = null
|
lastPushedPresenceState: R | null = null
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|
||||||
|
import { default as throttle } from 'lodash.throttle';
|
||||||
|
import { default as uniq } from 'lodash.uniq';
|
||||||
|
|
||||||
// @internal
|
// @internal
|
||||||
export function annotateError(error: unknown, annotations: Partial<ErrorAnnotations>): void;
|
export function annotateError(error: unknown, annotations: Partial<ErrorAnnotations>): void;
|
||||||
|
|
||||||
|
@ -76,6 +79,10 @@ export type Expand<T> = T extends infer O ? {
|
||||||
[K in keyof O]: O[K];
|
[K in keyof O]: O[K];
|
||||||
} : never;
|
} : never;
|
||||||
|
|
||||||
|
// @internal
|
||||||
|
function fetch_2(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
||||||
|
export { fetch_2 as fetch }
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export class FileHelpers {
|
export class FileHelpers {
|
||||||
static blobToDataUrl(file: Blob): Promise<string>;
|
static blobToDataUrl(file: Blob): Promise<string>;
|
||||||
|
@ -92,7 +99,13 @@ export function filterEntries<Key extends string, Value>(object: {
|
||||||
};
|
};
|
||||||
|
|
||||||
// @internal
|
// @internal
|
||||||
export function fpsThrottle(fn: () => void): () => void;
|
export function fpsThrottle(fn: {
|
||||||
|
(): void;
|
||||||
|
cancel?(): void;
|
||||||
|
}): {
|
||||||
|
(): void;
|
||||||
|
cancel?(): void;
|
||||||
|
};
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export function getErrorAnnotations(error: Error): ErrorAnnotations;
|
export function getErrorAnnotations(error: Error): ErrorAnnotations;
|
||||||
|
@ -145,6 +158,10 @@ export function getOwnProperty(obj: object, key: string): unknown;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export function hasOwnProperty(obj: object, key: string): boolean;
|
export function hasOwnProperty(obj: object, key: string): boolean;
|
||||||
|
|
||||||
|
// @internal
|
||||||
|
const Image_2: (width?: number, height?: number) => HTMLImageElement;
|
||||||
|
export { Image_2 as Image }
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export type IndexKey = string & {
|
export type IndexKey = string & {
|
||||||
__orderKey: true;
|
__orderKey: true;
|
||||||
|
@ -367,8 +384,7 @@ export const STRUCTURED_CLONE_OBJECT_PROTOTYPE: any;
|
||||||
const structuredClone_2: <T>(i: T) => T;
|
const structuredClone_2: <T>(i: T) => T;
|
||||||
export { structuredClone_2 as structuredClone }
|
export { structuredClone_2 as structuredClone }
|
||||||
|
|
||||||
// @public
|
export { throttle }
|
||||||
export function throttle<T extends (...args: any) => any>(func: T, limit: number): (...args: Parameters<T>) => ReturnType<T>;
|
|
||||||
|
|
||||||
// @internal
|
// @internal
|
||||||
export function throttleToNextFrame(fn: () => void): () => void;
|
export function throttleToNextFrame(fn: () => void): () => void;
|
||||||
|
@ -385,6 +401,8 @@ export class Timers {
|
||||||
setTimeout(handler: TimerHandler, timeout?: number, ...args: any[]): number;
|
setTimeout(handler: TimerHandler, timeout?: number, ...args: any[]): number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { uniq }
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export function validateIndexKey(key: string): asserts key is IndexKey;
|
export function validateIndexKey(key: string): asserts key is IndexKey;
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,13 @@
|
||||||
"^~(.*)": "<rootDir>/src/$1"
|
"^~(.*)": "<rootDir>/src/$1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"lodash.uniq": "^4.5.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/lodash.throttle": "^4.1.7",
|
||||||
|
"@types/lodash.uniq": "^4.5.7",
|
||||||
"jest-environment-jsdom": "^29.4.3",
|
"jest-environment-jsdom": "^29.4.3",
|
||||||
"lazyrepo": "0.0.0-alpha.27"
|
"lazyrepo": "0.0.0-alpha.27"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
export { default as throttle } from 'lodash.throttle'
|
||||||
|
export { default as uniq } from 'lodash.uniq'
|
||||||
export { PerformanceTracker } from './lib/PerformanceTracker'
|
export { PerformanceTracker } from './lib/PerformanceTracker'
|
||||||
export {
|
export {
|
||||||
areArraysShallowEqual,
|
areArraysShallowEqual,
|
||||||
|
@ -21,7 +23,7 @@ export {
|
||||||
export { debounce } from './lib/debounce'
|
export { debounce } from './lib/debounce'
|
||||||
export { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'
|
export { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'
|
||||||
export { FileHelpers } from './lib/file'
|
export { FileHelpers } from './lib/file'
|
||||||
export { noop, omitFromStackTrace, throttle } from './lib/function'
|
export { noop, omitFromStackTrace } from './lib/function'
|
||||||
export { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'
|
export { getHashForBuffer, getHashForObject, getHashForString, lns } from './lib/hash'
|
||||||
export { getFirstFromIterable } from './lib/iterable'
|
export { getFirstFromIterable } from './lib/iterable'
|
||||||
export type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'
|
export type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'
|
||||||
|
@ -32,6 +34,7 @@ export {
|
||||||
MediaHelpers,
|
MediaHelpers,
|
||||||
} from './lib/media/media'
|
} from './lib/media/media'
|
||||||
export { PngHelpers } from './lib/media/png'
|
export { PngHelpers } from './lib/media/png'
|
||||||
|
export { Image, fetch } from './lib/network'
|
||||||
export { invLerp, lerp, modulate, rng } from './lib/number'
|
export { invLerp, lerp, modulate, rng } from './lib/number'
|
||||||
export {
|
export {
|
||||||
areObjectsShallowEqual,
|
areObjectsShallowEqual,
|
||||||
|
|
|
@ -11,6 +11,7 @@ export class PerformanceTracker {
|
||||||
recordFrame = () => {
|
recordFrame = () => {
|
||||||
this.frames++
|
this.frames++
|
||||||
if (!this.started) return
|
if (!this.started) return
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
this.frame = requestAnimationFrame(this.recordFrame)
|
this.frame = requestAnimationFrame(this.recordFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ export class PerformanceTracker {
|
||||||
this.frames = 0
|
this.frames = 0
|
||||||
this.started = true
|
this.started = true
|
||||||
if (this.frame !== null) cancelAnimationFrame(this.frame)
|
if (this.frame !== null) cancelAnimationFrame(this.frame)
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
this.frame = requestAnimationFrame(this.recordFrame)
|
this.frame = requestAnimationFrame(this.recordFrame)
|
||||||
this.startTime = performance.now()
|
this.startTime = performance.now()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ export function debounce<T extends unknown[], U>(
|
||||||
let state:
|
let state:
|
||||||
| undefined
|
| undefined
|
||||||
| {
|
| {
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
timeout: ReturnType<typeof setTimeout>
|
timeout: ReturnType<typeof setTimeout>
|
||||||
promise: Promise<U>
|
promise: Promise<U>
|
||||||
resolve: (value: U | PromiseLike<U>) => void
|
resolve: (value: U | PromiseLike<U>) => void
|
||||||
|
@ -34,6 +35,8 @@ export function debounce<T extends unknown[], U>(
|
||||||
}
|
}
|
||||||
clearTimeout(state!.timeout)
|
clearTimeout(state!.timeout)
|
||||||
state!.latestArgs = args
|
state!.latestArgs = args
|
||||||
|
// It's up to the consumer of debounce to call `cancel`
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
state!.timeout = setTimeout(() => {
|
state!.timeout = setTimeout(() => {
|
||||||
const s = state!
|
const s = state!
|
||||||
state = undefined
|
state = undefined
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { fetch } from './network'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helpers for files
|
* Helpers for files
|
||||||
*
|
*
|
||||||
|
@ -10,11 +12,9 @@ export class FileHelpers {
|
||||||
* from https://stackoverflow.com/a/53817185
|
* from https://stackoverflow.com/a/53817185
|
||||||
*/
|
*/
|
||||||
static async dataUrlToArrayBuffer(dataURL: string) {
|
static async dataUrlToArrayBuffer(dataURL: string) {
|
||||||
return fetch(dataURL, { referrerPolicy: 'strict-origin-when-cross-origin' }).then(
|
return fetch(dataURL).then(function (result) {
|
||||||
function (result) {
|
return result.arrayBuffer()
|
||||||
return result.arrayBuffer()
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,31 +1,3 @@
|
||||||
/**
|
|
||||||
* Throttle a function.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* const A = throttle(myFunction, 1000)
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @see source - https://github.com/bameyrick/throttle-typescript
|
|
||||||
*/
|
|
||||||
export function throttle<T extends (...args: any) => any>(
|
|
||||||
func: T,
|
|
||||||
limit: number
|
|
||||||
): (...args: Parameters<T>) => ReturnType<T> {
|
|
||||||
let inThrottle: boolean
|
|
||||||
let lastResult: ReturnType<T>
|
|
||||||
return function (this: any, ...args: any[]): ReturnType<T> {
|
|
||||||
if (!inThrottle) {
|
|
||||||
inThrottle = true
|
|
||||||
setTimeout(() => (inThrottle = false), limit)
|
|
||||||
lastResult = func(...args)
|
|
||||||
}
|
|
||||||
return lastResult
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When a function is wrapped in `omitFromStackTrace`, if it throws an error the stack trace won't
|
* When a function is wrapped in `omitFromStackTrace`, if it throws an error the stack trace won't
|
||||||
* include the function itself or any stack frames above it. Useful for assertion-style function
|
* include the function itself or any stack frames above it. Useful for assertion-style function
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Image } from '../network'
|
||||||
import { isApngAnimated } from './apng'
|
import { isApngAnimated } from './apng'
|
||||||
import { isAvifAnimated } from './avif'
|
import { isAvifAnimated } from './avif'
|
||||||
import { isGifAnimated } from './gif'
|
import { isGifAnimated } from './gif'
|
||||||
|
@ -65,7 +66,7 @@ export class MediaHelpers {
|
||||||
*/
|
*/
|
||||||
static loadImage(src: string): Promise<HTMLImageElement> {
|
static loadImage(src: string): Promise<HTMLImageElement> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image()
|
const img = Image()
|
||||||
img.onload = () => resolve(img)
|
img.onload = () => resolve(img)
|
||||||
img.onerror = (e) => {
|
img.onerror = (e) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|
26
packages/utils/src/lib/network.ts
Normal file
26
packages/utils/src/lib/network.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Just a wrapper around `window.fetch` that sets the `referrerPolicy` to `strict-origin-when-cross-origin`.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export async function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
return window.fetch(input, {
|
||||||
|
// We want to make sure that the referrer is not sent to other domains.
|
||||||
|
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||||
|
...init,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just a wrapper around `new Image`, and yeah, it's a bit strange that it's in the network.ts file
|
||||||
|
* but the main concern here is the referrerPolicy and setting it correctly.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const Image = (width?: number, height?: number) => {
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
const img = new window.Image(width, height)
|
||||||
|
img.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||||
|
return img
|
||||||
|
}
|
|
@ -26,12 +26,16 @@ function tick() {
|
||||||
const elapsed = now - last
|
const elapsed = now - last
|
||||||
|
|
||||||
if (time + elapsed < targetTimePerFrame) {
|
if (time + elapsed < targetTimePerFrame) {
|
||||||
|
// It's up to the consumer of debounce to call `cancel`
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
frame = requestAnimationFrame(() => {
|
frame = requestAnimationFrame(() => {
|
||||||
frame = undefined
|
frame = undefined
|
||||||
tick()
|
tick()
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// It's up to the consumer of debounce to call `cancel`
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
frame = requestAnimationFrame(() => {
|
frame = requestAnimationFrame(() => {
|
||||||
frame = undefined
|
frame = undefined
|
||||||
last = now
|
last = now
|
||||||
|
@ -51,12 +55,16 @@ let started = false
|
||||||
* @returns
|
* @returns
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function fpsThrottle(fn: () => void) {
|
export function fpsThrottle(fn: { (): void; cancel?(): void }): {
|
||||||
|
(): void
|
||||||
|
cancel?(): void
|
||||||
|
} {
|
||||||
if (isTest()) {
|
if (isTest()) {
|
||||||
|
fn.cancel = () => frame && cancelAnimationFrame(frame)
|
||||||
return fn
|
return fn
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
const throttledFn = () => {
|
||||||
if (fpsQueue.includes(fn)) {
|
if (fpsQueue.includes(fn)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -68,6 +76,13 @@ export function fpsThrottle(fn: () => void) {
|
||||||
}
|
}
|
||||||
tick()
|
tick()
|
||||||
}
|
}
|
||||||
|
throttledFn.cancel = () => {
|
||||||
|
const index = fpsQueue.indexOf(fn)
|
||||||
|
if (index > -1) {
|
||||||
|
fpsQueue.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return throttledFn
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable no-restricted-properties */
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export class Timers {
|
export class Timers {
|
||||||
private timeouts: number[] = []
|
private timeouts: number[] = []
|
||||||
|
|
|
@ -6,6 +6,8 @@ import { nicelog } from './lib/nicelog'
|
||||||
import { getAllWorkspacePackages } from './lib/workspace'
|
import { getAllWorkspacePackages } from './lib/workspace'
|
||||||
|
|
||||||
const packagesOurTypesCanDependOn = [
|
const packagesOurTypesCanDependOn = [
|
||||||
|
'@types/lodash.throttle',
|
||||||
|
'@types/lodash.uniq',
|
||||||
'@types/react',
|
'@types/react',
|
||||||
'@types/react-dom',
|
'@types/react-dom',
|
||||||
'eventemitter3',
|
'eventemitter3',
|
||||||
|
|
|
@ -6129,8 +6129,6 @@ __metadata:
|
||||||
"@tldraw/validate": "workspace:*"
|
"@tldraw/validate": "workspace:*"
|
||||||
"@types/benchmark": "npm:^2.1.2"
|
"@types/benchmark": "npm:^2.1.2"
|
||||||
"@types/core-js": "npm:^2.5.5"
|
"@types/core-js": "npm:^2.5.5"
|
||||||
"@types/lodash.throttle": "npm:^4.1.7"
|
|
||||||
"@types/lodash.uniq": "npm:^4.5.7"
|
|
||||||
"@types/react-test-renderer": "npm:^18.0.0"
|
"@types/react-test-renderer": "npm:^18.0.0"
|
||||||
"@types/wicg-file-system-access": "npm:^2020.9.5"
|
"@types/wicg-file-system-access": "npm:^2020.9.5"
|
||||||
"@use-gesture/react": "npm:^10.2.27"
|
"@use-gesture/react": "npm:^10.2.27"
|
||||||
|
@ -6144,8 +6142,6 @@ __metadata:
|
||||||
jest-canvas-mock: "npm:^2.5.2"
|
jest-canvas-mock: "npm:^2.5.2"
|
||||||
jest-environment-jsdom: "npm:^29.4.3"
|
jest-environment-jsdom: "npm:^29.4.3"
|
||||||
lazyrepo: "npm:0.0.0-alpha.27"
|
lazyrepo: "npm:0.0.0-alpha.27"
|
||||||
lodash.throttle: "npm:^4.1.1"
|
|
||||||
lodash.uniq: "npm:^4.5.0"
|
|
||||||
nanoid: "npm:4.0.2"
|
nanoid: "npm:4.0.2"
|
||||||
react-test-renderer: "npm:^18.2.0"
|
react-test-renderer: "npm:^18.2.0"
|
||||||
resize-observer-polyfill: "npm:^1.5.1"
|
resize-observer-polyfill: "npm:^1.5.1"
|
||||||
|
@ -6322,8 +6318,12 @@ __metadata:
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@tldraw/utils@workspace:packages/utils"
|
resolution: "@tldraw/utils@workspace:packages/utils"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@types/lodash.throttle": "npm:^4.1.7"
|
||||||
|
"@types/lodash.uniq": "npm:^4.5.7"
|
||||||
jest-environment-jsdom: "npm:^29.4.3"
|
jest-environment-jsdom: "npm:^29.4.3"
|
||||||
lazyrepo: "npm:0.0.0-alpha.27"
|
lazyrepo: "npm:0.0.0-alpha.27"
|
||||||
|
lodash.throttle: "npm:^4.1.1"
|
||||||
|
lodash.uniq: "npm:^4.5.0"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue