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: [
|
||||
'prettier',
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@next/next/core-web-vitals',
|
||||
],
|
||||
|
@ -12,6 +13,7 @@ module.exports = {
|
|||
'import',
|
||||
'local',
|
||||
'@next/next',
|
||||
'react',
|
||||
'react-hooks',
|
||||
'deprecation',
|
||||
],
|
||||
|
@ -23,15 +25,17 @@ module.exports = {
|
|||
rules: {
|
||||
'deprecation/deprecation': 'error',
|
||||
'@next/next/no-html-link-for-pages': 'off',
|
||||
'react/jsx-key': 'off',
|
||||
'no-non-null-assertion': 'off',
|
||||
'no-fallthrough': 'off',
|
||||
'react/jsx-no-target-blank': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/no-fallthrough': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'react/display-name': 'off',
|
||||
'@next/next/no-img-element': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/no-extra-semi': 'off',
|
||||
'no-mixed-spaces-and-tabs': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
|
@ -86,39 +90,107 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/editor/**/*', 'packages/tldraw/**/*'],
|
||||
files: ['packages/editor/**/*', 'packages/tldraw/**/*', 'packages/utils/**/*'],
|
||||
rules: {
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
{
|
||||
name: 'setInterval',
|
||||
message: 'Use the timers from @tldraw/util instead.',
|
||||
name: 'fetch',
|
||||
message: 'Use the fetch from @tldraw/util instead.',
|
||||
},
|
||||
{
|
||||
name: 'Image',
|
||||
message: 'Use the Image from @tldraw/util instead.',
|
||||
},
|
||||
{
|
||||
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',
|
||||
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': [
|
||||
'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',
|
||||
property: 'setTimeout',
|
||||
message: 'Use the timers from @tldraw/util instead.',
|
||||
message: 'Use the timers from editor.timers instead.',
|
||||
},
|
||||
{
|
||||
object: 'window',
|
||||
property: 'setInterval',
|
||||
message: 'Use the timers from @tldraw/util instead.',
|
||||
message: 'Use the timers from editor.timers instead.',
|
||||
},
|
||||
{
|
||||
object: 'window',
|
||||
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: {
|
||||
'no-restricted-properties': '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.{' '}
|
||||
</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>
|
||||
"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
|
||||
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
|
||||
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
|
||||
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>
|
||||
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
|
||||
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
|
||||
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,
|
||||
non-infringement, merchantability, or fitness for a particular purpose.{' '}
|
||||
</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.
|
||||
</p>
|
||||
<p>
|
||||
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
|
||||
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
|
||||
such phrase is not used to promote the derivative works or to imply that tldraw endorses
|
||||
you or the derivative works.
|
||||
</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
|
||||
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.
|
||||
|
|
|
@ -15,7 +15,7 @@ export default async function NotFound() {
|
|||
<div className="page-header">
|
||||
<h1>Not found.</h1>
|
||||
</div>
|
||||
<p>There's nothing here. :(</p>
|
||||
<p>There’s nothing here. :(</p>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,6 +24,7 @@ export function Header({ sectionId }: { sectionId?: string }) {
|
|||
href="https://x.com/tldraw/"
|
||||
className="sidebar__button icon-button"
|
||||
title="twitter"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon icon="twitter" />
|
||||
|
@ -32,6 +33,7 @@ export function Header({ sectionId }: { sectionId?: string }) {
|
|||
href="https://discord.com/invite/SBBEVCA4PG"
|
||||
className="sidebar__button icon-button"
|
||||
title="discord"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon icon="discord" />
|
||||
|
@ -40,6 +42,7 @@ export function Header({ sectionId }: { sectionId?: string }) {
|
|||
href="https://github.com/tldraw/tldraw"
|
||||
className="sidebar__button icon-button"
|
||||
title="github"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon icon="github" />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
||||
import { RoomSnapshot } from '@tldraw/tlsync'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Tldraw } from 'tldraw'
|
||||
import { Tldraw, fetch } from 'tldraw'
|
||||
import '../../../styles/core.css'
|
||||
import { assetUrls } from '../../utils/assetUrls'
|
||||
import { useFileSystem } from '../../utils/useFileSystem'
|
||||
|
|
|
@ -62,7 +62,7 @@ export function IFrameProtector({
|
|||
return (
|
||||
<div className="tldraw__editor tl-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'}
|
||||
<svg
|
||||
width="15"
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
TldrawUiMenuContextProvider,
|
||||
TldrawUiMenuGroup,
|
||||
TldrawUiMenuItem,
|
||||
fetch,
|
||||
unwrapLabel,
|
||||
useActions,
|
||||
useContainer,
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
MediaHelpers,
|
||||
TLAsset,
|
||||
TLAssetId,
|
||||
fetch,
|
||||
getHashForString,
|
||||
uniqueId,
|
||||
} from 'tldraw'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
||||
import { RoomSnapshot } from '@tldraw/tlsync'
|
||||
import { fetch } from 'tldraw'
|
||||
import '../../styles/globals.css'
|
||||
import { BoardHistorySnapshot } from '../components/BoardHistorySnapshot/BoardHistorySnapshot'
|
||||
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
|
||||
import { fetch } from 'tldraw'
|
||||
import { BoardHistoryLog } from '../components/BoardHistoryLog/BoardHistoryLog'
|
||||
import { ErrorPage } from '../components/ErrorPage/ErrorPage'
|
||||
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 { IFrameProtector, ROOM_CONTEXT } from '../components/IFrameProtector'
|
||||
import { SnapshotsEditor } from '../components/SnapshotsEditor'
|
||||
|
|
|
@ -2,7 +2,7 @@ import { CreateRoomRequestBody, ROOM_PREFIX, Snapshot } from '@tldraw/dotcom-sha
|
|||
import { schema } from '@tldraw/tlsync'
|
||||
import { useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { TldrawUiButton } from 'tldraw'
|
||||
import { TldrawUiButton, fetch } from 'tldraw'
|
||||
import '../../styles/globals.css'
|
||||
import { getParentOrigin } from '../utils/iFrame'
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { getAssetUrlsByImport } from '@tldraw/assets/imports.vite'
|
||||
import { Image } from 'tldraw'
|
||||
|
||||
export const assetUrls = getAssetUrlsByImport()
|
||||
|
||||
|
@ -12,7 +13,7 @@ async function preloadIcons() {
|
|||
|
||||
function preloadIcon(url: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image()
|
||||
const image = Image()
|
||||
// this isn't known by typescript but it works
|
||||
;(image as any).fetchPriority = 'low'
|
||||
image.onload = resolve
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TLAsset } from 'tldraw'
|
||||
import { TLAsset, fetch } from 'tldraw'
|
||||
|
||||
export async function cloneAssetForShare(
|
||||
asset: TLAsset,
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
MediaHelpers,
|
||||
TLAsset,
|
||||
TLAssetId,
|
||||
fetch,
|
||||
getHashForString,
|
||||
uniqueId,
|
||||
} from 'tldraw'
|
||||
|
@ -18,7 +19,6 @@ export async function createAssetFromFile({ file }: { type: 'file'; file: File }
|
|||
await fetch(url, {
|
||||
method: 'POST',
|
||||
body: file,
|
||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||
})
|
||||
|
||||
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'
|
||||
|
||||
interface ResponseBody {
|
||||
|
@ -45,7 +45,6 @@ export async function createAssetFromUrl({ url }: { type: 'url'; url: string }):
|
|||
const resp = await fetch(url, {
|
||||
method: 'GET',
|
||||
mode: 'no-cors',
|
||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||
})
|
||||
const html = await resp.text()
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { openDB } from 'idb'
|
||||
import { Editor, TLV1Document, buildFromV1Document } from 'tldraw'
|
||||
import { Editor, TLV1Document, buildFromV1Document, fetch } from 'tldraw'
|
||||
|
||||
export function isEditorEmpty(editor: Editor) {
|
||||
const hasAnyShapes = editor.store.allRecords().some((r) => r.typeName === 'shape')
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
TLUiOverrides,
|
||||
TLUiToastsContextType,
|
||||
TLUiTranslationKey,
|
||||
fetch,
|
||||
isShape,
|
||||
} from 'tldraw'
|
||||
import { useMultiplayerAssets } from '../hooks/useMultiplayerAssets'
|
||||
|
|
|
@ -45,6 +45,7 @@ export function ExamplePage({
|
|||
<a
|
||||
target="_blank"
|
||||
href="https://twitter.com/tldraw"
|
||||
rel="noopener noreferrer"
|
||||
title="twitter"
|
||||
className="hoverable"
|
||||
>
|
||||
|
@ -53,6 +54,7 @@ export function ExamplePage({
|
|||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/tldraw/tldraw"
|
||||
rel="noopener noreferrer"
|
||||
title="github"
|
||||
className="hoverable"
|
||||
>
|
||||
|
@ -61,6 +63,7 @@ export function ExamplePage({
|
|||
<a
|
||||
target="_blank"
|
||||
href="https://discord.com/invite/SBBEVCA4PG"
|
||||
rel="noopener noreferrer"
|
||||
title="discord"
|
||||
className="hoverable"
|
||||
>
|
||||
|
@ -104,6 +107,7 @@ export function ExamplePage({
|
|||
<a
|
||||
className="example__sidebar__footer-link example__sidebar__footer-link--grey"
|
||||
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+"
|
||||
>
|
||||
Request an example
|
||||
|
@ -111,6 +115,7 @@ export function ExamplePage({
|
|||
<a
|
||||
className="example__sidebar__footer-link example__sidebar__footer-link--grey"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://tldraw.dev"
|
||||
>
|
||||
Visit the docs
|
||||
|
|
|
@ -42,10 +42,10 @@ function ControlledFocusExample() {
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
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.
|
||||
</p>
|
||||
<div style={{ width: 800, maxWidth: '100%', height: 500 }}>
|
||||
|
@ -67,7 +67,7 @@ function FreeFocusExample() {
|
|||
<div>
|
||||
<h2>Free Focus</h2>
|
||||
<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.
|
||||
</p>
|
||||
<div
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { throttle } from 'lodash'
|
||||
import { useLayoutEffect, useState } from 'react'
|
||||
import { Tldraw, createTLStore, getSnapshot, loadSnapshot, throttle } from 'tldraw'
|
||||
import { Tldraw, createTLStore, getSnapshot, loadSnapshot } from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
|
||||
// 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
|
||||
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
|
||||
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
|
||||
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.
|
||||
</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
|
||||
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
|
||||
|
@ -169,7 +169,7 @@ function ABunchOfText() {
|
|||
merged into a harmonious symphony.
|
||||
</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
|
||||
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
|
||||
|
@ -179,7 +179,7 @@ function ABunchOfText() {
|
|||
take shape.
|
||||
</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
|
||||
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
|
||||
|
|
|
@ -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)',
|
||||
}}
|
||||
>
|
||||
<p>This won't scale with zoom.</p>
|
||||
<p>This won’t scale with zoom.</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export default function YjsExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
We've moved! See{' '}
|
||||
We’ve moved! See{' '}
|
||||
<a href="https://github.com/tldraw/tldraw-yjs-example">
|
||||
https://github.com/tldraw/tldraw-yjs-example
|
||||
</a>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { AssetRecordType, TLAsset, TLExternalAssetContent, getHashForString } from 'tldraw'
|
||||
import { AssetRecordType, TLAsset, TLExternalAssetContent, fetch, getHashForString } from 'tldraw'
|
||||
import { rpc } from './rpc'
|
||||
|
||||
export async function onCreateAssetFromUrl({
|
||||
|
@ -30,7 +30,6 @@ export async function onCreateAssetFromUrl({
|
|||
const resp = await fetch(url, {
|
||||
method: 'GET',
|
||||
mode: 'no-cors',
|
||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||
})
|
||||
const html = await resp.text()
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
|
|
|
@ -1073,6 +1073,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
};
|
||||
interrupt(): this;
|
||||
isAncestorSelected(shape: TLShape | TLShapeId): boolean;
|
||||
isDisposed: boolean;
|
||||
isIn(path: string): boolean;
|
||||
isInAny(...paths: string[]): boolean;
|
||||
isPointInShape(shape: TLShape | TLShapeId, point: VecLike, opts?: {
|
||||
|
|
|
@ -57,8 +57,6 @@
|
|||
"eventemitter3": "^4.0.7",
|
||||
"idb": "^7.1.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash.uniq": "^4.5.0",
|
||||
"nanoid": "4.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -70,8 +68,6 @@
|
|||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@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/wicg-file-system-access": "^2020.9.5",
|
||||
"benchmark": "^2.1.4",
|
||||
|
|
|
@ -596,6 +596,7 @@ function DebugSvgCopy({ id }: { id: TLShapeId }) {
|
|||
src={image.src}
|
||||
width={image.bounds.width}
|
||||
height={image.bounds.height}
|
||||
referrerPolicy="no-referrer"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
|
|
@ -230,6 +230,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
this.options = { ...defaultTldrawOptions, ...options }
|
||||
this.store = store
|
||||
this.disposables.add(this.store.dispose.bind(this.store))
|
||||
this.history = new HistoryManager<TLRecord>({
|
||||
store,
|
||||
annotateError: (error) => {
|
||||
|
@ -718,6 +719,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*/
|
||||
readonly disposables = new Set<() => void>()
|
||||
|
||||
/**
|
||||
* Whether the editor is disposed.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
isDisposed = false
|
||||
|
||||
/** @internal */
|
||||
private readonly _tickManager
|
||||
|
||||
|
@ -798,6 +806,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
dispose() {
|
||||
this.disposables.forEach((dispose) => dispose())
|
||||
this.disposables.clear()
|
||||
this.isDisposed = true
|
||||
}
|
||||
|
||||
/* ------------------- Shape Utils ------------------ */
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import throttle from 'lodash.throttle'
|
||||
import { throttle } from '@tldraw/utils'
|
||||
import { useLayoutEffect } from 'react'
|
||||
import { Box } from '../primitives/Box'
|
||||
import { useEditor } from './useEditor'
|
||||
|
@ -62,6 +62,7 @@ export function useScreenBounds(ref: React.RefObject<HTMLElement>) {
|
|||
window.removeEventListener('resize', updateBounds)
|
||||
resizeObserver.disconnect()
|
||||
scrollingParent?.removeEventListener('scroll', updateBounds)
|
||||
updateBounds.cancel()
|
||||
}
|
||||
}, [editor, ref])
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export function useZoomCss() {
|
|||
|
||||
return () => {
|
||||
scheduler.detach()
|
||||
setScaleDebounced.cancel()
|
||||
}
|
||||
}, [editor, container])
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { fetch } from '@tldraw/utils'
|
||||
|
||||
/** @public */
|
||||
export function dataUrlToFile(url: string, filename: string, mimeType: string) {
|
||||
return fetch(url)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import _uniq from 'lodash.uniq'
|
||||
import { uniq as _uniq } from '@tldraw/utils'
|
||||
|
||||
/** @public */
|
||||
export function uniq<T>(
|
||||
|
|
|
@ -348,6 +348,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
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>;
|
||||
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)
|
||||
ensureStoreIsUsable(): void;
|
||||
extractingChanges(fn: () => void): RecordsDiff<R>;
|
||||
|
|
|
@ -162,6 +162,15 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
*/
|
||||
private historyReactor: Reactor
|
||||
|
||||
/**
|
||||
* Function to dispose of any in-flight timeouts.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
private cancelHistoryReactor: () => void = () => {
|
||||
/* noop */
|
||||
}
|
||||
|
||||
readonly schema: StoreSchema<R, 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
|
||||
this._flushHistory()
|
||||
},
|
||||
{ scheduleEffect: (cb) => throttleToNextFrame(cb) }
|
||||
{ scheduleEffect: (cb) => (this.cancelHistoryReactor = throttleToNextFrame(cb)) }
|
||||
)
|
||||
this.scopedTypes = {
|
||||
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.
|
||||
* @param change - the records diff
|
||||
|
|
|
@ -186,7 +186,13 @@ export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
|
|||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
assert,
|
||||
compact,
|
||||
createShapeId,
|
||||
fetch,
|
||||
getHashForBuffer,
|
||||
getHashForString,
|
||||
} from '@tldraw/editor'
|
||||
|
@ -116,7 +117,6 @@ export function registerDefaultExternalContentHandlers(
|
|||
const resp = await fetch(url, {
|
||||
method: 'GET',
|
||||
mode: 'no-cors',
|
||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||
})
|
||||
const html = await resp.text()
|
||||
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) => {
|
||||
if (editor.isDisposed) return
|
||||
|
||||
const { url } = shape.props
|
||||
|
||||
// Create the asset using the external content manager's createAssetFromUrl method.
|
||||
|
|
|
@ -3,11 +3,13 @@ import {
|
|||
BaseBoxShapeUtil,
|
||||
FileHelpers,
|
||||
HTMLContainer,
|
||||
Image,
|
||||
MediaHelpers,
|
||||
TLImageShape,
|
||||
TLOnDoubleClickHandler,
|
||||
TLShapePartial,
|
||||
Vec,
|
||||
fetch,
|
||||
imageShapeMigrations,
|
||||
imageShapeProps,
|
||||
structuredClone,
|
||||
|
@ -19,7 +21,7 @@ import { HyperlinkButton } from '../shared/HyperlinkButton'
|
|||
import { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion'
|
||||
|
||||
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()
|
||||
return FileHelpers.blobToDataUrl(blob)
|
||||
}
|
||||
|
@ -70,7 +72,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
const url = asset.props.src
|
||||
if (!url) return
|
||||
|
||||
const image = new Image()
|
||||
const image = Image()
|
||||
image.onload = () => {
|
||||
if (cancelled) return
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
TLDefaultFontStyle,
|
||||
TLShapeUtilCanvasSvgDef,
|
||||
debugFlags,
|
||||
fetch,
|
||||
last,
|
||||
useEditor,
|
||||
useValue,
|
||||
|
@ -28,9 +29,7 @@ export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef
|
|||
const fontFaceRule: string = (font as any).$$_fontface
|
||||
if (!url || !fontFaceRule) return null
|
||||
|
||||
const fontFile = await (
|
||||
await fetch(url, { referrerPolicy: 'strict-origin-when-cross-origin' })
|
||||
).blob()
|
||||
const fontFile = await (await fetch(url)).blob()
|
||||
const base64FontFile = await FileHelpers.blobToDataUrl(fontFile)
|
||||
|
||||
const newFontFaceRule = fontFaceRule.replace(url, base64FontFile)
|
||||
|
|
|
@ -40,7 +40,6 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function
|
|||
spellCheck="true"
|
||||
wrap="off"
|
||||
dir="auto"
|
||||
datatype="wysiwyg"
|
||||
defaultValue={text}
|
||||
onFocus={handleFocus}
|
||||
onChange={handleChange}
|
||||
|
|
|
@ -21,6 +21,10 @@ export class Idle extends StateNode {
|
|||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
|
||||
override onExit = () => {
|
||||
updateHoveredShapeId.cancel()
|
||||
}
|
||||
|
||||
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
|
||||
if (info.key === 'Enter') {
|
||||
if (this.editor.getInstanceState().isReadonly) return null
|
||||
|
|
|
@ -23,6 +23,8 @@ export class EditingShape extends StateNode {
|
|||
// Clear the editing shape
|
||||
this.editor.setEditingShape(null)
|
||||
|
||||
updateHoveredShapeId.cancel()
|
||||
|
||||
const shape = this.editor.getShape(editingShapeId)!
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
|
||||
|
|
|
@ -41,6 +41,10 @@ export class Idle extends StateNode {
|
|||
this.editor.setCursor({ type: 'default', rotation: 0 })
|
||||
}
|
||||
|
||||
override onExit = () => {
|
||||
updateHoveredShapeId.cancel()
|
||||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
updateHoveredShapeId(this.editor)
|
||||
}
|
||||
|
|
|
@ -32,5 +32,7 @@ function _updateHoveredShapeId(editor: Editor) {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
export const updateHoveredShapeId =
|
||||
process.env.NODE_ENV === 'test' ? _updateHoveredShapeId : throttle(_updateHoveredShapeId, 32)
|
||||
export const updateHoveredShapeId = throttle(
|
||||
_updateHoveredShapeId,
|
||||
process.env.NODE_ENV === 'test' ? 0 : 32
|
||||
)
|
||||
|
|
|
@ -240,7 +240,7 @@ export function ExampleDialog({
|
|||
style={{ marginRight: 'auto' }}
|
||||
>
|
||||
<TldrawUiButtonCheck checked={dontShowAgain} />
|
||||
<TldrawUiButtonLabel>Don't show again</TldrawUiButtonLabel>
|
||||
<TldrawUiButtonLabel>Don’t show again</TldrawUiButtonLabel>
|
||||
</TldrawUiButton>
|
||||
)}
|
||||
<TldrawUiButton type="normal" onClick={onCancel}>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Image } from '@tldraw/editor'
|
||||
import { createContext, useContext, useEffect } from 'react'
|
||||
import { TLUiAssetUrls } from '../assetUrls'
|
||||
|
||||
|
@ -16,13 +17,13 @@ export function AssetUrlsProvider({
|
|||
}) {
|
||||
useEffect(() => {
|
||||
for (const src of Object.values(assetUrls.icons)) {
|
||||
const image = new Image()
|
||||
const image = Image()
|
||||
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||
image.src = src
|
||||
image.decode()
|
||||
}
|
||||
for (const src of Object.values(assetUrls.embedIcons)) {
|
||||
const image = new Image()
|
||||
const image = Image()
|
||||
image.referrerPolicy = 'strict-origin-when-cross-origin'
|
||||
image.src = src
|
||||
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
|
||||
|
@ -14,12 +14,7 @@ export async function pasteFiles(
|
|||
point?: VecLike,
|
||||
sources?: TLExternalContentSource[]
|
||||
) {
|
||||
const blobs = await Promise.all(
|
||||
urls.map(
|
||||
async (url) =>
|
||||
await (await fetch(url, { referrerPolicy: 'strict-origin-when-cross-origin' })).blob()
|
||||
)
|
||||
)
|
||||
const blobs = await Promise.all(urls.map(async (url) => await (await fetch(url)).blob()))
|
||||
const files = blobs.map((blob) => new File([blob], 'tldrawFile', { type: blob.type }))
|
||||
|
||||
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'
|
||||
|
||||
/**
|
||||
|
@ -22,7 +22,6 @@ export async function pasteUrl(
|
|||
if (new URL(url).pathname.match(/\.(png|jpe?g|gif|svg|webp)$/i)) {
|
||||
const resp = await fetch(url, {
|
||||
method: 'HEAD',
|
||||
referrerPolicy: 'strict-origin-when-cross-origin',
|
||||
})
|
||||
if (resp.headers.get('content-type')?.match(/^image\//)) {
|
||||
editor.mark('paste')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { LANGUAGES } from '@tldraw/editor'
|
||||
import { LANGUAGES, fetch } from '@tldraw/editor'
|
||||
import { TLUiAssetUrls } from '../../assetUrls'
|
||||
import { TLUiTranslationKey } from './TLUiTranslationKey'
|
||||
import { DEFAULT_TRANSLATION } from './defaultTranslation'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
Editor,
|
||||
Image,
|
||||
PngHelpers,
|
||||
TLShapeId,
|
||||
TLSvgOptions,
|
||||
|
@ -34,7 +35,7 @@ export async function getSvgAsImage(
|
|||
const svgUrl = URL.createObjectURL(new Blob([svgString], { type: 'image/svg+xml' }))
|
||||
|
||||
const canvas = await new Promise<HTMLCanvasElement | null>((resolve) => {
|
||||
const image = new Image()
|
||||
const image = Image()
|
||||
image.crossOrigin = 'anonymous'
|
||||
|
||||
image.onload = async () => {
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
VecModel,
|
||||
clamp,
|
||||
createShapeId,
|
||||
fetch,
|
||||
structuredClone,
|
||||
} from '@tldraw/editor'
|
||||
import { getArrowBindings } from '../../shapes/arrow/shared'
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
UnknownRecord,
|
||||
createTLStore,
|
||||
exhaustiveSwitchError,
|
||||
fetch,
|
||||
partition,
|
||||
} from '@tldraw/editor'
|
||||
import { TLUiToastsContextType } from '../../ui/context/toasts'
|
||||
|
@ -188,9 +189,7 @@ export async function serializeTldrawJson(store: TLStore): Promise<string> {
|
|||
try {
|
||||
// try to save the asset as a base64 string
|
||||
assetSrcToSave = await FileHelpers.blobToDataUrl(
|
||||
await (
|
||||
await fetch(record.props.src, { referrerPolicy: 'strict-origin-when-cross-origin' })
|
||||
).blob()
|
||||
await (await fetch(record.props.src)).blob()
|
||||
)
|
||||
} catch {
|
||||
// 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() {
|
||||
this.debug('closing')
|
||||
this.disposables.forEach((dispose) => dispose())
|
||||
this.flushPendingPushRequests.cancel?.()
|
||||
this.scheduleRebase.cancel?.()
|
||||
}
|
||||
|
||||
lastPushedPresenceState: R | null = null
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
```ts
|
||||
|
||||
import { default as throttle } from 'lodash.throttle';
|
||||
import { default as uniq } from 'lodash.uniq';
|
||||
|
||||
// @internal
|
||||
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];
|
||||
} : never;
|
||||
|
||||
// @internal
|
||||
function fetch_2(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
||||
export { fetch_2 as fetch }
|
||||
|
||||
// @public
|
||||
export class FileHelpers {
|
||||
static blobToDataUrl(file: Blob): Promise<string>;
|
||||
|
@ -92,7 +99,13 @@ export function filterEntries<Key extends string, Value>(object: {
|
|||
};
|
||||
|
||||
// @internal
|
||||
export function fpsThrottle(fn: () => void): () => void;
|
||||
export function fpsThrottle(fn: {
|
||||
(): void;
|
||||
cancel?(): void;
|
||||
}): {
|
||||
(): void;
|
||||
cancel?(): void;
|
||||
};
|
||||
|
||||
// @internal (undocumented)
|
||||
export function getErrorAnnotations(error: Error): ErrorAnnotations;
|
||||
|
@ -145,6 +158,10 @@ export function getOwnProperty(obj: object, key: string): unknown;
|
|||
// @internal (undocumented)
|
||||
export function hasOwnProperty(obj: object, key: string): boolean;
|
||||
|
||||
// @internal
|
||||
const Image_2: (width?: number, height?: number) => HTMLImageElement;
|
||||
export { Image_2 as Image }
|
||||
|
||||
// @public
|
||||
export type IndexKey = string & {
|
||||
__orderKey: true;
|
||||
|
@ -367,8 +384,7 @@ export const STRUCTURED_CLONE_OBJECT_PROTOTYPE: any;
|
|||
const structuredClone_2: <T>(i: T) => T;
|
||||
export { structuredClone_2 as structuredClone }
|
||||
|
||||
// @public
|
||||
export function throttle<T extends (...args: any) => any>(func: T, limit: number): (...args: Parameters<T>) => ReturnType<T>;
|
||||
export { throttle }
|
||||
|
||||
// @internal
|
||||
export function throttleToNextFrame(fn: () => void): () => void;
|
||||
|
@ -385,6 +401,8 @@ export class Timers {
|
|||
setTimeout(handler: TimerHandler, timeout?: number, ...args: any[]): number;
|
||||
}
|
||||
|
||||
export { uniq }
|
||||
|
||||
// @internal (undocumented)
|
||||
export function validateIndexKey(key: string): asserts key is IndexKey;
|
||||
|
||||
|
|
|
@ -49,7 +49,13 @@
|
|||
"^~(.*)": "<rootDir>/src/$1"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash.uniq": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.throttle": "^4.1.7",
|
||||
"@types/lodash.uniq": "^4.5.7",
|
||||
"jest-environment-jsdom": "^29.4.3",
|
||||
"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 {
|
||||
areArraysShallowEqual,
|
||||
|
@ -21,7 +23,7 @@ export {
|
|||
export { debounce } from './lib/debounce'
|
||||
export { annotateError, getErrorAnnotations, type ErrorAnnotations } from './lib/error'
|
||||
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 { getFirstFromIterable } from './lib/iterable'
|
||||
export type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'
|
||||
|
@ -32,6 +34,7 @@ export {
|
|||
MediaHelpers,
|
||||
} from './lib/media/media'
|
||||
export { PngHelpers } from './lib/media/png'
|
||||
export { Image, fetch } from './lib/network'
|
||||
export { invLerp, lerp, modulate, rng } from './lib/number'
|
||||
export {
|
||||
areObjectsShallowEqual,
|
||||
|
|
|
@ -11,6 +11,7 @@ export class PerformanceTracker {
|
|||
recordFrame = () => {
|
||||
this.frames++
|
||||
if (!this.started) return
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
this.frame = requestAnimationFrame(this.recordFrame)
|
||||
}
|
||||
|
||||
|
@ -19,6 +20,7 @@ export class PerformanceTracker {
|
|||
this.frames = 0
|
||||
this.started = true
|
||||
if (this.frame !== null) cancelAnimationFrame(this.frame)
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
this.frame = requestAnimationFrame(this.recordFrame)
|
||||
this.startTime = performance.now()
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export function debounce<T extends unknown[], U>(
|
|||
let state:
|
||||
| undefined
|
||||
| {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
timeout: ReturnType<typeof setTimeout>
|
||||
promise: Promise<U>
|
||||
resolve: (value: U | PromiseLike<U>) => void
|
||||
|
@ -34,6 +35,8 @@ export function debounce<T extends unknown[], U>(
|
|||
}
|
||||
clearTimeout(state!.timeout)
|
||||
state!.latestArgs = args
|
||||
// It's up to the consumer of debounce to call `cancel`
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
state!.timeout = setTimeout(() => {
|
||||
const s = state!
|
||||
state = undefined
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { fetch } from './network'
|
||||
|
||||
/**
|
||||
* Helpers for files
|
||||
*
|
||||
|
@ -10,11 +12,9 @@ export class FileHelpers {
|
|||
* from https://stackoverflow.com/a/53817185
|
||||
*/
|
||||
static async dataUrlToArrayBuffer(dataURL: string) {
|
||||
return fetch(dataURL, { referrerPolicy: 'strict-origin-when-cross-origin' }).then(
|
||||
function (result) {
|
||||
return result.arrayBuffer()
|
||||
}
|
||||
)
|
||||
return fetch(dataURL).then(function (result) {
|
||||
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
|
||||
* 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 { isAvifAnimated } from './avif'
|
||||
import { isGifAnimated } from './gif'
|
||||
|
@ -65,7 +66,7 @@ export class MediaHelpers {
|
|||
*/
|
||||
static loadImage(src: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
const img = Image()
|
||||
img.onload = () => resolve(img)
|
||||
img.onerror = (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
|
||||
|
||||
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 = undefined
|
||||
tick()
|
||||
})
|
||||
return
|
||||
}
|
||||
// It's up to the consumer of debounce to call `cancel`
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
frame = requestAnimationFrame(() => {
|
||||
frame = undefined
|
||||
last = now
|
||||
|
@ -51,12 +55,16 @@ let started = false
|
|||
* @returns
|
||||
* @internal
|
||||
*/
|
||||
export function fpsThrottle(fn: () => void) {
|
||||
export function fpsThrottle(fn: { (): void; cancel?(): void }): {
|
||||
(): void
|
||||
cancel?(): void
|
||||
} {
|
||||
if (isTest()) {
|
||||
fn.cancel = () => frame && cancelAnimationFrame(frame)
|
||||
return fn
|
||||
}
|
||||
|
||||
return () => {
|
||||
const throttledFn = () => {
|
||||
if (fpsQueue.includes(fn)) {
|
||||
return
|
||||
}
|
||||
|
@ -68,6 +76,13 @@ export function fpsThrottle(fn: () => void) {
|
|||
}
|
||||
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 */
|
||||
export class Timers {
|
||||
private timeouts: number[] = []
|
||||
|
|
|
@ -6,6 +6,8 @@ import { nicelog } from './lib/nicelog'
|
|||
import { getAllWorkspacePackages } from './lib/workspace'
|
||||
|
||||
const packagesOurTypesCanDependOn = [
|
||||
'@types/lodash.throttle',
|
||||
'@types/lodash.uniq',
|
||||
'@types/react',
|
||||
'@types/react-dom',
|
||||
'eventemitter3',
|
||||
|
|
|
@ -6129,8 +6129,6 @@ __metadata:
|
|||
"@tldraw/validate": "workspace:*"
|
||||
"@types/benchmark": "npm:^2.1.2"
|
||||
"@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/wicg-file-system-access": "npm:^2020.9.5"
|
||||
"@use-gesture/react": "npm:^10.2.27"
|
||||
|
@ -6144,8 +6142,6 @@ __metadata:
|
|||
jest-canvas-mock: "npm:^2.5.2"
|
||||
jest-environment-jsdom: "npm:^29.4.3"
|
||||
lazyrepo: "npm:0.0.0-alpha.27"
|
||||
lodash.throttle: "npm:^4.1.1"
|
||||
lodash.uniq: "npm:^4.5.0"
|
||||
nanoid: "npm:4.0.2"
|
||||
react-test-renderer: "npm:^18.2.0"
|
||||
resize-observer-polyfill: "npm:^1.5.1"
|
||||
|
@ -6322,8 +6318,12 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "@tldraw/utils@workspace:packages/utils"
|
||||
dependencies:
|
||||
"@types/lodash.throttle": "npm:^4.1.7"
|
||||
"@types/lodash.uniq": "npm:^4.5.7"
|
||||
jest-environment-jsdom: "npm:^29.4.3"
|
||||
lazyrepo: "npm:0.0.0-alpha.27"
|
||||
lodash.throttle: "npm:^4.1.1"
|
||||
lodash.uniq: "npm:^4.5.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
|
Loading…
Reference in a new issue