Electron App (#224)

* add electron wrapper

* add to workspaces

* fixes electron setup

* Fix package for dev

* build out electron app communication

* Update README.md
This commit is contained in:
Steve Ruiz 2021-11-07 13:45:48 +00:00 committed by GitHub
parent 0d20994de9
commit 7c980ebb19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 4120 additions and 96 deletions

21
electron/LICENSE.md Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Stephen Ruiz Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
electron/README.md Normal file
View file

@ -0,0 +1,11 @@
# @tldraw/tldraw-electron
An electron wrapper for TLDraw. Not yet published.
## Development
From the root of the repository, run:
```bash
yarn start:vscode
```

View file

@ -0,0 +1,11 @@
mainConfig:
type: esbuild
path: esbuild.main.config.ts
src: src/main/main.ts
output: dist/main
rendererConfig:
type: esbuild
path: esbuild.renderer.config.ts
html: src/renderer/index.html
src: src/renderer/index.tsx
output: dist/renderer

View file

@ -0,0 +1,12 @@
import { BuildOptions } from 'esbuild'
import path from 'path'
const config: BuildOptions = {
platform: 'node',
entryPoints: [path.resolve('src/main/main.ts'), path.resolve('src/main/preload.ts')],
bundle: true,
target: 'node16.5.0', // electron version target
sourcemap: true,
}
export default config

View file

@ -0,0 +1,12 @@
import { BuildOptions } from 'esbuild'
import path from 'path'
const config: BuildOptions = {
platform: 'browser',
entryPoints: [path.resolve('src/renderer/index.tsx')],
bundle: true,
target: 'chrome94', // electron version target
sourcemap: true,
}
export default config

78
electron/package.json Normal file
View file

@ -0,0 +1,78 @@
{
"name": "@tldraw/electron",
"version": "0.1.4",
"private": true,
"description": "An electron app for tldraw.",
"author": "@steveruizok",
"license": "MIT",
"keywords": [
"react",
"typescript",
"esbuild"
],
"scripts": {
"start:electron": "yarn dev",
"dev": "electron-esbuild dev",
"build": "electron-esbuild build",
"package": "electron-builder"
},
"devDependencies": {
"@tldraw/tldraw": "^0.1.4",
"@types/node": "^14.14.35",
"@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9",
"@types/react-router-dom": "^5.1.8",
"electron": "15.3.0",
"electron-builder": "^22.13.1",
"electron-esbuild": "^3.0.0",
"electron-util": "^0.17.2",
"esbuild": "^0.13.8",
"esbuild-serve": "^1.0.1",
"react": ">=16.8",
"react-dom": "^16.8 || ^17.0",
"rimraf": "3.0.2",
"typescript": "4.2.3"
},
"build": {
"appId": "io.comp.tldraw-electron",
"productName": "TLDraw",
"extraMetadata": {
"name": "TLDraw",
"main": "main.js"
},
"files": [
{
"from": ".",
"filter": [
"package.json"
]
},
{
"from": "dist/main"
},
{
"from": "dist/renderer"
}
],
"win": {
"target": [
"zip"
]
},
"mac": {
"target": [
"zip"
]
},
"linux": {
"target": [
"zip"
]
},
"directories": {
"buildResources": "resources"
},
"publish": null
},
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

Binary file not shown.

BIN
electron/resources/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View file

@ -0,0 +1,20 @@
/* eslint-disable */
require('dotenv').config()
const { notarize } = require('electron-notarize')
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== 'darwin') {
return
}
const appName = context.packager.appInfo.productFilename
return await notarize({
appBundleId: 'com.tldraw.app',
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLEID,
appleIdPassword: process.env.APPLEIDPASS,
})
}

View file

@ -0,0 +1,114 @@
import { shell, app, Menu, MenuItemConstructorOptions } from 'electron'
import type { Message } from 'src/types'
export async function createMenu(send: (message: Message) => Promise<void>) {
const isMac = process.platform === 'darwin'
const template: MenuItemConstructorOptions[] = []
// About Menu (mac only)
if (isMac) {
template.push({
label: 'Hello world!',
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
})
}
// File Menu
template.push({
label: 'File',
submenu: [
{ label: 'New Project', click: () => send({ type: 'undo' }) },
{ type: 'separator' },
{ label: 'Open...', click: () => send({ type: 'redo' }) },
{ type: 'separator' },
{ label: 'Save', click: () => send({ type: 'redo' }) },
{ label: 'Save As...', click: () => send({ type: 'redo' }) },
{ type: 'separator' },
{ role: 'quit' },
],
})
// Edit Menu
template.push({
label: 'Edit',
submenu: [
{ label: 'Undo', click: () => send({ type: 'undo' }), accelerator: 'CmdOrCtrl+Z' },
{ label: 'Redo', click: () => send({ type: 'redo' }), accelerator: 'CmdOrCtrl+Shift+Z' },
{ type: 'separator' },
{ label: 'Cut', click: () => send({ type: 'cut' }), accelerator: 'CmdOrCtrl+X' },
{ label: 'Copy', click: () => send({ type: 'copy' }), accelerator: 'CmdOrCtrl+C' },
{ label: 'Paste', click: () => send({ type: 'paste' }), accelerator: 'CmdOrCtrl+V' },
{ label: 'Delete', click: () => send({ type: 'delete' }), accelerator: 'Delete' },
{ label: 'Select All', click: () => send({ type: 'selectAll' }), accelerator: 'CmdOrCtrl+A' },
{ label: 'Select None', click: () => send({ type: 'selectNone' }) },
],
})
// View Menu
template.push({
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{
label: 'Actual Size',
click: () => send({ type: 'resetZoom' }),
},
{ label: 'Zoom In', click: () => send({ type: 'zoomIn' }) },
{ label: 'Zoom Out', click: () => send({ type: 'zoomOut' }) },
{ label: 'Zoom to Fit', click: () => send({ type: 'zoomToFit' }) },
{ label: 'Zoom to Selection', click: () => send({ type: 'zoomToSelection' }) },
{ type: 'separator' },
{ role: 'togglefullscreen' },
],
})
// Window Menu
if (isMac) {
template.push({
label: 'Window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
{ type: 'separator' },
{ role: 'front' },
{ type: 'separator' },
{ role: 'window' },
],
})
} else {
template.push({
label: 'Window',
submenu: [{ role: 'minimize' }, { role: 'zoom' }, { role: 'close' }],
})
}
template.push({
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
await shell.openExternal('https://electronjs.org')
},
},
],
})
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}

View file

@ -0,0 +1,58 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import path from 'path'
import { format } from 'url'
import { BrowserWindow } from 'electron'
import { is } from 'electron-util'
export async function createWindow() {
let win: BrowserWindow | null = null
win = new BrowserWindow({
width: 720,
height: 450,
minHeight: 480,
minWidth: 600,
titleBarStyle: 'hidden',
title: 'TLDraw',
webPreferences: {
nodeIntegration: true,
devTools: true,
preload: path.join(__dirname, 'preload.js'),
},
frame: false,
show: false,
})
win.setWindowButtonVisibility(false)
const isDev = is.development
if (isDev) {
win.loadURL('http://localhost:9080')
} else {
win.loadURL(path.join(__dirname, 'index.html'))
}
win.setPosition(0, 0, false)
win.setSize(700, 1200)
win.on('closed', () => {
win = null
})
win.webContents.on('devtools-opened', () => {
win!.focus()
})
win.on('ready-to-show', () => {
win!.show()
if (isDev) {
win!.webContents.openDevTools({ mode: 'bottom' })
} else {
win!.focus()
}
})
return win
}

32
electron/src/main/main.ts Normal file
View file

@ -0,0 +1,32 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { app, BrowserWindow } from 'electron'
import { is } from 'electron-util'
import type { Message } from 'src/types'
import { createMenu } from './createMenu'
import { createWindow } from './createWindow'
import './preload'
let win: BrowserWindow | null = null
async function main() {
win = await createWindow()
async function send(message: Message) {
win!.webContents.send('projectMsg', message)
}
await createMenu(send)
}
app
.on('ready', main)
.on('window-all-closed', () => {
if (!is.macos) {
app.quit()
}
})
.on('activate', () => {
if (win === null && app.isReady()) {
main()
}
})

View file

@ -0,0 +1,15 @@
import { contextBridge, ipcRenderer } from 'electron'
import type { Message, TLApi } from 'src/types'
const api: TLApi = {
send: (channel: string, data: Message) => {
ipcRenderer.send(channel, data)
},
on: (channel, cb) => {
ipcRenderer.on(channel, (event, message) => cb(message as Message))
},
}
contextBridge?.exposeInMainWorld('TLApi', api)
export {}

View file

@ -0,0 +1,85 @@
import * as React from 'react'
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
import type { IpcMainEvent, IpcMain, IpcRenderer } from 'electron'
import type { Message, TLApi } from 'src/types'
export default function App(): JSX.Element {
const rTLDrawState = React.useRef<TLDrawState>()
// When the editor mounts, save the tlstate instance in a ref.
const handleMount = React.useCallback((tldr: TLDrawState) => {
rTLDrawState.current = tldr
}, [])
React.useEffect(() => {
function handleEvent(message: Message) {
const tlstate = rTLDrawState.current
if (!tlstate) return
switch (message.type) {
case 'resetZoom': {
tlstate.resetZoom()
break
}
case 'zoomIn': {
tlstate.zoomIn()
break
}
case 'zoomOut': {
tlstate.zoomOut()
break
}
case 'zoomToFit': {
tlstate.zoomToFit()
break
}
case 'zoomToSelection': {
tlstate.zoomToSelection()
break
}
case 'undo': {
tlstate.undo()
break
}
case 'redo': {
tlstate.redo()
break
}
case 'cut': {
tlstate.cut()
break
}
case 'copy': {
tlstate.copy()
break
}
case 'paste': {
tlstate.paste()
break
}
case 'delete': {
tlstate.delete()
break
}
case 'selectAll': {
tlstate.selectAll()
break
}
case 'selectNone': {
tlstate.selectNone()
break
}
}
}
const { send, on } = (window as unknown as Window & { TLApi: TLApi })['TLApi']
on('projectMsg', handleEvent)
})
return (
<div className="tldraw">
<TLDraw id="electron" onMount={handleMount} autofocus showMenu={false} />
</div>
)
}

0
electron/src/renderer/decs.d.ts vendored Normal file
View file

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="index.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="index.js"></script>
</body>
</html>

View file

@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'
import './styles.css'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)

View file

@ -0,0 +1,20 @@
html,
* {
box-sizing: border-box;
}
body {
overscroll-behavior: none;
margin: 0px;
padding: 0px;
}
.tldraw {
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
width: 100%;
height: 100%;
}

19
electron/src/types.ts Normal file
View file

@ -0,0 +1,19 @@
export type Message =
| { type: 'zoomIn' }
| { type: 'zoomOut' }
| { type: 'resetZoom' }
| { type: 'zoomToFit' }
| { type: 'zoomToSelection' }
| { type: 'undo' }
| { type: 'redo' }
| { type: 'cut' }
| { type: 'copy' }
| { type: 'paste' }
| { type: 'delete' }
| { type: 'selectAll' }
| { type: 'selectNone' }
export type TLApi = {
send: (channel: string, data: Message) => void
on: (channel: string, cb: (message: Message) => void) => void
}

24
electron/tsconfig.json Normal file
View file

@ -0,0 +1,24 @@
{
"extends": "../tsconfig.base.json",
"include": ["src"],
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"outDir": "./dist/types",
"rootDir": ".",
"baseUrl": ".",
"allowJs": false,
"emitDeclarationOnly": true,
"paths": {
"@tldraw/tldraw": ["../packages/tldraw"]
}
},
"references": [
{
"path": "../packages/tldraw"
}
],
"typedocOptions": {
"entryPoints": ["src/index.ts"],
"out": "docs"
}
}

1892
electron/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
}, },
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"electron",
"vscode/editor", "vscode/editor",
"packages/tldraw", "packages/tldraw",
"example", "example",
@ -18,6 +19,7 @@
"test": "jest", "test": "jest",
"test:watch": "jest --watchAll", "test:watch": "jest --watchAll",
"lerna": "lerna", "lerna": "lerna",
"start:electron": "lerna run start:electron --stream --parallel",
"start:vscode": "lerna run start:vscode --stream --parallel", "start:vscode": "lerna run start:vscode --stream --parallel",
"start": "lerna run start --stream --parallel", "start": "lerna run start --stream --parallel",
"start:www": "yarn build:packages && lerna run start --parallel & cd www && yarn dev", "start:www": "yarn build:packages && lerna run start --parallel & cd www && yarn dev",

View file

@ -22,6 +22,7 @@
"module": "./dist/esm/index.js", "module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts", "types": "./dist/types/index.d.ts",
"scripts": { "scripts": {
"start:electron": "yarn start",
"start:vscode": "yarn start", "start:vscode": "yarn start",
"start": "node scripts/dev & yarn types:dev", "start": "node scripts/dev & yarn types:dev",
"build": "node scripts/build && yarn types:build && node scripts/copy-readme", "build": "node scripts/build && yarn types:build && node scripts/copy-readme",

View file

@ -24,6 +24,10 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
callbacks.onSignOut?.(tlstate) callbacks.onSignOut?.(tlstate)
}, [tlstate]) }, [tlstate])
const handleCut = React.useCallback(() => {
tlstate.cut()
}, [tlstate])
const handleCopy = React.useCallback(() => { const handleCopy = React.useCallback(() => {
tlstate.copy() tlstate.copy()
}, [tlstate]) }, [tlstate])
@ -44,8 +48,8 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
tlstate.selectAll() tlstate.selectAll()
}, [tlstate]) }, [tlstate])
const handleDeselectAll = React.useCallback(() => { const handleselectNone = React.useCallback(() => {
tlstate.deselectAll() tlstate.selectNone()
}, [tlstate]) }, [tlstate])
const showFileMenu = const showFileMenu =
@ -96,6 +100,9 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
Redo Redo
</DMItem> </DMItem>
<DMDivider dir="ltr" /> <DMDivider dir="ltr" />
<DMItem onSelect={handleCut} kbd="#X">
Cut
</DMItem>
<DMItem onSelect={handleCopy} kbd="#C"> <DMItem onSelect={handleCopy} kbd="#C">
Copy Copy
</DMItem> </DMItem>
@ -111,7 +118,7 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
<DMItem onSelect={handleSelectAll} kbd="#A"> <DMItem onSelect={handleSelectAll} kbd="#A">
Select All Select All
</DMItem> </DMItem>
<DMItem onSelect={handleDeselectAll}>Select None</DMItem> <DMItem onSelect={handleselectNone}>Select None</DMItem>
</DMSubMenu> </DMSubMenu>
<DMDivider dir="ltr" /> <DMDivider dir="ltr" />
</> </>

View file

@ -24,7 +24,7 @@ export function ZoomMenu() {
<DMItem onSelect={tlstate.zoomOut} kbd="#"> <DMItem onSelect={tlstate.zoomOut} kbd="#">
Zoom Out Zoom Out
</DMItem> </DMItem>
<DMItem onSelect={tlstate.zoomToActual} kbd="⇧0"> <DMItem onSelect={tlstate.resetZoom} kbd="⇧0">
To 100% To 100%
</DMItem> </DMItem>
<DMItem onSelect={tlstate.zoomToFit} kbd="⇧1"> <DMItem onSelect={tlstate.zoomToFit} kbd="⇧1">

View file

@ -249,7 +249,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
useHotkeys( useHotkeys(
'shift+0', 'shift+0',
() => { () => {
if (canHandleEvent()) tlstate.zoomToActual() if (canHandleEvent()) tlstate.resetZoom()
}, },
undefined, undefined,
[tlstate] [tlstate]
@ -398,7 +398,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
[tlstate] [tlstate]
) )
// Copy & Paste // Copy, Cut & Paste
useHotkeys( useHotkeys(
'command+c,ctrl+c', 'command+c,ctrl+c',
@ -409,6 +409,15 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
[tlstate] [tlstate]
) )
useHotkeys(
'command+x,ctrl+x',
() => {
if (canHandleEvent()) tlstate.cut()
},
undefined,
[tlstate]
)
useHotkeys( useHotkeys(
'command+v,ctrl+v', 'command+v,ctrl+v',
() => { () => {

View file

@ -11,7 +11,7 @@ describe('TLDrawState', () => {
describe('When copying and pasting...', () => { describe('When copying and pasting...', () => {
it('copies a shape', () => { it('copies a shape', () => {
tlstate.loadDocument(mockDocument).deselectAll().copy(['rect1']) tlstate.loadDocument(mockDocument).selectNone().copy(['rect1'])
}) })
it('pastes a shape', () => { it('pastes a shape', () => {
@ -19,7 +19,7 @@ describe('TLDrawState', () => {
const prevCount = Object.keys(tlstate.page.shapes).length const prevCount = Object.keys(tlstate.page.shapes).length
tlstate.deselectAll().copy(['rect1']).paste() tlstate.selectNone().copy(['rect1']).paste()
expect(Object.keys(tlstate.page.shapes).length).toBe(prevCount + 1) expect(Object.keys(tlstate.page.shapes).length).toBe(prevCount + 1)
@ -35,7 +35,7 @@ describe('TLDrawState', () => {
it('pastes a shape to a new page', () => { it('pastes a shape to a new page', () => {
tlstate.loadDocument(mockDocument) tlstate.loadDocument(mockDocument)
tlstate.deselectAll().copy(['rect1']).createPage().paste() tlstate.selectNone().copy(['rect1']).createPage().paste()
expect(Object.keys(tlstate.page.shapes).length).toBe(1) expect(Object.keys(tlstate.page.shapes).length).toBe(1)
@ -135,14 +135,14 @@ describe('TLDrawState', () => {
describe('Selection', () => { describe('Selection', () => {
it('selects a shape', () => { it('selects a shape', () => {
tlstate.loadDocument(mockDocument).deselectAll() tlstate.loadDocument(mockDocument).selectNone()
tlu.clickShape('rect1') tlu.clickShape('rect1')
expect(tlstate.selectedIds).toStrictEqual(['rect1']) expect(tlstate.selectedIds).toStrictEqual(['rect1'])
expect(tlstate.appState.status).toBe('idle') expect(tlstate.appState.status).toBe('idle')
}) })
it('selects and deselects a shape', () => { it('selects and deselects a shape', () => {
tlstate.loadDocument(mockDocument).deselectAll() tlstate.loadDocument(mockDocument).selectNone()
tlu.clickShape('rect1') tlu.clickShape('rect1')
tlu.clickCanvas() tlu.clickCanvas()
expect(tlstate.selectedIds).toStrictEqual([]) expect(tlstate.selectedIds).toStrictEqual([])
@ -150,7 +150,7 @@ describe('TLDrawState', () => {
}) })
it('selects multiple shapes', () => { it('selects multiple shapes', () => {
tlstate.loadDocument(mockDocument).deselectAll() tlstate.loadDocument(mockDocument).selectNone()
tlu.clickShape('rect1') tlu.clickShape('rect1')
tlu.clickShape('rect2', { shiftKey: true }) tlu.clickShape('rect2', { shiftKey: true })
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2']) expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
@ -158,7 +158,7 @@ describe('TLDrawState', () => {
}) })
it('shift-selects to deselect shapes', () => { it('shift-selects to deselect shapes', () => {
tlstate.loadDocument(mockDocument).deselectAll() tlstate.loadDocument(mockDocument).selectNone()
tlu.clickShape('rect1') tlu.clickShape('rect1')
tlu.clickShape('rect2', { shiftKey: true }) tlu.clickShape('rect2', { shiftKey: true })
tlu.clickShape('rect2', { shiftKey: true }) tlu.clickShape('rect2', { shiftKey: true })
@ -167,7 +167,7 @@ describe('TLDrawState', () => {
}) })
it('clears selection when clicking bounds', () => { it('clears selection when clicking bounds', () => {
tlstate.loadDocument(mockDocument).deselectAll() tlstate.loadDocument(mockDocument).selectNone()
tlstate.startSession(SessionType.Brush, [-10, -10]) tlstate.startSession(SessionType.Brush, [-10, -10])
tlstate.updateSession([110, 110]) tlstate.updateSession([110, 110])
tlstate.completeSession() tlstate.completeSession()
@ -187,7 +187,7 @@ describe('TLDrawState', () => {
// }) // })
it('does not select on meta-click', () => { it('does not select on meta-click', () => {
tlstate.loadDocument(mockDocument).deselectAll() tlstate.loadDocument(mockDocument).selectNone()
tlu.clickShape('rect1', { ctrlKey: true }) tlu.clickShape('rect1', { ctrlKey: true })
expect(tlstate.selectedIds).toStrictEqual([]) expect(tlstate.selectedIds).toStrictEqual([])
expect(tlstate.appState.status).toBe('idle') expect(tlstate.appState.status).toBe('idle')
@ -214,7 +214,7 @@ describe('TLDrawState', () => {
// Single click on a selected shape to select just that shape // Single click on a selected shape to select just that shape
it('single-selects shape in selection on click', () => { it('single-selects shape in selection on click', () => {
tlstate.deselectAll() tlstate.selectNone()
tlu.clickShape('rect1') tlu.clickShape('rect1')
tlu.clickShape('rect2', { shiftKey: true }) tlu.clickShape('rect2', { shiftKey: true })
tlu.clickShape('rect2') tlu.clickShape('rect2')
@ -223,7 +223,7 @@ describe('TLDrawState', () => {
}) })
it('single-selects shape in selection on pointerup only', () => { it('single-selects shape in selection on pointerup only', () => {
tlstate.deselectAll() tlstate.selectNone()
tlu.clickShape('rect1') tlu.clickShape('rect1')
tlu.clickShape('rect2', { shiftKey: true }) tlu.clickShape('rect2', { shiftKey: true })
tlu.pointShape('rect2') tlu.pointShape('rect2')
@ -234,7 +234,7 @@ describe('TLDrawState', () => {
}) })
// it('selects shapes if shift key is lifted before pointerup', () => { // it('selects shapes if shift key is lifted before pointerup', () => {
// tlstate.deselectAll() // tlstate.selectNone()
// tlu.clickShape('rect1') // tlu.clickShape('rect1')
// tlu.pointShape('rect2', { shiftKey: true }) // tlu.pointShape('rect2', { shiftKey: true })
// expect(tlstate.appState.status).toBe('pointingBounds') // expect(tlstate.appState.status).toBe('pointingBounds')
@ -390,7 +390,7 @@ describe('TLDrawState', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
.loadDocument(mockDocument) .loadDocument(mockDocument)
.group(['rect1', 'rect2'], 'groupA') .group(['rect1', 'rect2'], 'groupA')
.deselectAll() .selectNone()
const tlu = new TLDrawStateUtils(tlstate) const tlu = new TLDrawStateUtils(tlstate)
@ -454,7 +454,7 @@ describe('TLDrawState', () => {
expect(tlstate.getShape('groupA').childIndex).toBe(2) expect(tlstate.getShape('groupA').childIndex).toBe(2)
tlstate.deselectAll() tlstate.selectNone()
tlstate.selectTool(TLDrawShapeType.Rectangle) tlstate.selectTool(TLDrawShapeType.Rectangle)
const prevB = tlstate.shapes.map((shape) => shape.id) const prevB = tlstate.shapes.map((shape) => shape.id)

View file

@ -685,7 +685,7 @@ export class TLDrawState extends StateManager<Data> {
if (!document.pages[pageId]) { if (!document.pages[pageId]) {
if (pageId === this.appState.currentPageId) { if (pageId === this.appState.currentPageId) {
this.cancelSession() this.cancelSession()
this.deselectAll() this.selectNone()
} }
currentPageStates[pageId] = undefined as unknown as TLPageState currentPageStates[pageId] = undefined as unknown as TLPageState
@ -812,7 +812,7 @@ export class TLDrawState extends StateManager<Data> {
* @param document The document to load * @param document The document to load
*/ */
loadDocument = (document: TLDrawDocument): this => { loadDocument = (document: TLDrawDocument): this => {
this.deselectAll() this.selectNone()
this.resetHistory() this.resetHistory()
this.clearSelectHistory() this.clearSelectHistory()
this.session = undefined this.session = undefined
@ -1165,6 +1165,16 @@ export class TLDrawState extends StateManager<Data> {
return this return this
} }
/**
* Cut (copy and delete) one or more shapes to the clipboard.
* @param ids The ids of the shapes to cut.
*/
cut = (ids = this.selectedIds): this => {
this.copy(ids)
this.delete(ids)
return this
}
/** /**
* Paste shapes (or text) from clipboard to a certain point. * Paste shapes (or text) from clipboard to a certain point.
* @param point * @param point
@ -1579,7 +1589,7 @@ export class TLDrawState extends StateManager<Data> {
/** /**
* Zoom the camera to 100%. * Zoom the camera to 100%.
*/ */
zoomToActual = (): this => { resetZoom = (): this => {
return this.zoomTo(1) return this.zoomTo(1)
} }
@ -1714,7 +1724,7 @@ export class TLDrawState extends StateManager<Data> {
/** /**
* Deselect any selected shapes. * Deselect any selected shapes.
*/ */
deselectAll = (): this => { selectNone = (): this => {
this.setSelectedIds([]) this.setSelectedIds([])
this.addToSelectHistory(this.selectedIds) this.addToSelectHistory(this.selectedIds)
return this return this

View file

@ -73,7 +73,7 @@ describe('Delete command', () => {
expect(Object.values(tlstate.page.bindings)[0]).toBe(undefined) expect(Object.values(tlstate.page.bindings)[0]).toBe(undefined)
tlstate tlstate
.deselectAll() .selectNone()
.createShapes({ .createShapes({
id: 'arrow1', id: 'arrow1',
type: TLDrawShapeType.Arrow, type: TLDrawShapeType.Arrow,

View file

@ -26,7 +26,7 @@ describe('Group command', () => {
describe('when less than two shapes are selected', () => { describe('when less than two shapes are selected', () => {
it('does nothing', () => { it('does nothing', () => {
tlstate.deselectAll() tlstate.selectNone()
// @ts-ignore // @ts-ignore
const stackLength = tlstate.stack.length const stackLength = tlstate.stack.length

View file

@ -47,12 +47,12 @@ describe('when running the command', () => {
.loadDocument(mockDocument) .loadDocument(mockDocument)
.select('rect1') .select('rect1')
.rotate() .rotate()
.deselectAll() .selectNone()
.undo() .undo()
expect(tlstate.selectedIds).toEqual(['rect1']) expect(tlstate.selectedIds).toEqual(['rect1'])
tlstate.deselectAll().redo() tlstate.selectNone().redo()
expect(tlstate.selectedIds).toEqual(['rect1']) expect(tlstate.selectedIds).toEqual(['rect1'])
}) })

View file

@ -72,12 +72,12 @@ describe('when running the command', () => {
.loadDocument(mockDocument) .loadDocument(mockDocument)
.select('rect1', 'rect2') .select('rect1', 'rect2')
.stretch(StretchType.Horizontal) .stretch(StretchType.Horizontal)
.deselectAll() .selectNone()
.undo() .undo()
expect(tlstate.selectedIds).toEqual(['rect1', 'rect2']) expect(tlstate.selectedIds).toEqual(['rect1', 'rect2'])
tlstate.deselectAll().redo() tlstate.selectNone().redo()
expect(tlstate.selectedIds).toEqual(['rect1', 'rect2']) expect(tlstate.selectedIds).toEqual(['rect1', 'rect2'])
}) })

View file

@ -82,12 +82,12 @@ describe('when running the command', () => {
.loadDocument(mockDocument) .loadDocument(mockDocument)
.select('rect1') .select('rect1')
.style({ size: SizeStyle.Small }) .style({ size: SizeStyle.Small })
.deselectAll() .selectNone()
.undo() .undo()
expect(tlstate.selectedIds).toEqual(['rect1']) expect(tlstate.selectedIds).toEqual(['rect1'])
tlstate.deselectAll().redo() tlstate.selectNone().redo()
expect(tlstate.selectedIds).toEqual(['rect1']) expect(tlstate.selectedIds).toEqual(['rect1'])
}) })

View file

@ -67,12 +67,12 @@ describe('when running the command', () => {
.loadDocument(mockDocument) .loadDocument(mockDocument)
.select('rect1') .select('rect1')
.toggleHidden() .toggleHidden()
.deselectAll() .selectNone()
.undo() .undo()
expect(tlstate.selectedIds).toEqual(['rect1']) expect(tlstate.selectedIds).toEqual(['rect1'])
tlstate.deselectAll().redo() tlstate.selectNone().redo()
expect(tlstate.selectedIds).toEqual(['rect1']) expect(tlstate.selectedIds).toEqual(['rect1'])
}) })

View file

@ -6,7 +6,7 @@ describe('Brush session', () => {
it('begins, updateSession', () => { it('begins, updateSession', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
.loadDocument(mockDocument) .loadDocument(mockDocument)
.deselectAll() .selectNone()
.startSession(SessionType.Brush, [-10, -10]) .startSession(SessionType.Brush, [-10, -10])
.updateSession([10, 10]) .updateSession([10, 10])
.completeSession() .completeSession()
@ -17,7 +17,7 @@ describe('Brush session', () => {
it('selects multiple shapes', () => { it('selects multiple shapes', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
.loadDocument(mockDocument) .loadDocument(mockDocument)
.deselectAll() .selectNone()
.startSession(SessionType.Brush, [-10, -10]) .startSession(SessionType.Brush, [-10, -10])
.updateSession([110, 110]) .updateSession([110, 110])
.completeSession() .completeSession()
@ -27,7 +27,7 @@ describe('Brush session', () => {
it('does not de-select original shapes', () => { it('does not de-select original shapes', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
.loadDocument(mockDocument) .loadDocument(mockDocument)
.deselectAll() .selectNone()
.select('rect1') .select('rect1')
.startSession(SessionType.Brush, [300, 300]) .startSession(SessionType.Brush, [300, 300])
.updateSession([301, 301]) .updateSession([301, 301])
@ -38,9 +38,9 @@ describe('Brush session', () => {
// it('does not select hidden shapes', () => { // it('does not select hidden shapes', () => {
// const tlstate = new TLDrawState() // const tlstate = new TLDrawState()
// .loadDocument(mockDocument) // .loadDocument(mockDocument)
// .deselectAll() // .selectNone()
// .toggleHidden(['rect1']) // .toggleHidden(['rect1'])
// .deselectAll() // .selectNone()
// .startSession(SessionType.Brush, [-10, -10]) // .startSession(SessionType.Brush, [-10, -10])
// .updateSession([10, 10]) // .updateSession([10, 10])
// .completeSession() // .completeSession()
@ -49,9 +49,9 @@ describe('Brush session', () => {
it('when command is held, require the entire shape to be selected', () => { it('when command is held, require the entire shape to be selected', () => {
const tlstate = new TLDrawState() const tlstate = new TLDrawState()
.loadDocument(mockDocument) .loadDocument(mockDocument)
.deselectAll() .selectNone()
.loadDocument(mockDocument) .loadDocument(mockDocument)
.deselectAll() .selectNone()
.startSession(SessionType.Brush, [-10, -10]) .startSession(SessionType.Brush, [-10, -10])
.updateSession([10, 10], false, false, true) .updateSession([10, 10], false, false, true)
.completeSession() .completeSession()

View file

@ -50,7 +50,7 @@ export class DrawUtil extends TLDrawShapeUtil<T, E> {
return style.dash === DashStyle.Draw return style.dash === DashStyle.Draw
? getDrawStrokePathData(shape) ? getDrawStrokePathData(shape)
: getSolidStrokePathData(shape) : getSolidStrokePathData(shape)
}, [points, style.size, style.dash, isComplete, false]) }, [points, style.size, style.dash, isComplete])
const styles = getShapeStyle(style, meta.isDarkMode) const styles = getShapeStyle(style, meta.isDarkMode)

View file

@ -56,7 +56,7 @@ describe('When double clicking link controls', () => {
.startSession(SessionType.Arrow, [200, 200], 'end') .startSession(SessionType.Arrow, [200, 200], 'end')
.updateSession([250, 50]) .updateSession([250, 50])
.completeSession() .completeSession()
.deselectAll().document .selectNone().document
it('moves all linked shapes when center is dragged', () => { it('moves all linked shapes when center is dragged', () => {
const tlstate = new TLDrawState().loadDocument(doc).select('rect2') const tlstate = new TLDrawState().loadDocument(doc).select('rect2')

View file

@ -63,8 +63,8 @@ export class SelectTool extends BaseTool<Status> {
this.state.select(...this.state.selectedIds.filter((oid) => oid !== shape.parentId), id) this.state.select(...this.state.selectedIds.filter((oid) => oid !== shape.parentId), id)
} }
private deselectAll() { private selectNone() {
this.state.deselectAll() this.state.selectNone()
} }
onEnter = () => { onEnter = () => {
@ -172,7 +172,7 @@ export class SelectTool extends BaseTool<Status> {
/* ----------------- Event Handlers ----------------- */ /* ----------------- Event Handlers ----------------- */
onCancel = () => { onCancel = () => {
this.deselectAll() this.selectNone()
this.state.cancelSession() this.state.cancelSession()
this.setStatus(Status.Idle) this.setStatus(Status.Idle)
} }
@ -379,7 +379,7 @@ export class SelectTool extends BaseTool<Status> {
if (info.target === 'bounds') { if (info.target === 'bounds') {
// If we just clicked the selecting bounds's background, // If we just clicked the selecting bounds's background,
// clear the selection // clear the selection
this.deselectAll() this.selectNone()
} else if (this.state.isSelected(info.target)) { } else if (this.state.isSelected(info.target)) {
// If we're holding shift... // If we're holding shift...
if (info.shiftKey) { if (info.shiftKey) {
@ -438,7 +438,7 @@ export class SelectTool extends BaseTool<Status> {
return return
} }
this.deselectAll() this.selectNone()
} }
this.setStatus(Status.PointingCanvas) this.setStatus(Status.PointingCanvas)
@ -494,7 +494,7 @@ export class SelectTool extends BaseTool<Status> {
if (info.metaKey) { if (info.metaKey) {
if (!info.shiftKey) { if (!info.shiftKey) {
this.deselectAll() this.selectNone()
} }
const point = this.state.getPagePoint(info.point) const point = this.state.getPagePoint(info.point)
@ -595,7 +595,7 @@ export class SelectTool extends BaseTool<Status> {
onPointBounds: TLBoundsEventHandler = (info) => { onPointBounds: TLBoundsEventHandler = (info) => {
if (info.metaKey) { if (info.metaKey) {
if (!info.shiftKey) { if (!info.shiftKey) {
this.deselectAll() this.selectNone()
} }
const point = this.state.getPagePoint(info.point) const point = this.state.getPagePoint(info.point)

1627
yarn.lock

File diff suppressed because it is too large Load diff